こんにちは。KOUKIです。
Go言語のGoroutineを使ったプログラムを書く時に、「思った通りの挙動をしないな」と思う時があります。
その代表例が数値のカウントアップです。
<目次>
事例: 数値が安全にカウントアップしない
まずは、事例をみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package main import ( "fmt" "runtime" "sync" "time" ) func main() { runtime.GOMAXPROCS(4) var counter uint64 var wg sync.WaitGroup start := time.Now() for i := 0; i < 100000; i++ { wg.Add(1) go func() { defer wg.Done() for c := 0; c < 10000; c++ { counter++ } }() } wg.Wait() end := time.Now() fmt.Println("counter: ", counter) fmt.Printf("%f秒\n", (end.Sub(start)).Seconds()) } |
上記のコードでは、10000個のGoroutineを生成し、counter変数に対して各自10000回のカウントアップを行なっています。
以下のコマンドでプログラムを複数回実行してみます。
1 2 3 4 5 6 7 8 9 10 11 |
$ go run main.go counter: 463598375 1.533456秒 $ go run main.go counter: 493101040 1.520631秒 $ go run main.go counter: 459220698 1.572800秒 |
ご覧の通り、カウンターの値がバラバラになってしまいました。これは、Goroutineによる変数への書き込みを行う際、変数(メモリ)の排他制御をしていないので、ほぼ同時に値を書き込むことによって、正しい値が変数に保持されないことによって起こります。
対策1: Mutexを使う
このような場合は、排他制御を行うことができるsync.Mutexを使います。これを使えば、変数にアクセスできるGoroutineは常に一つだけ、という状態を作れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package main import ( "fmt" "runtime" "sync" "time" ) func main() { runtime.GOMAXPROCS(4) var counter uint64 var wg sync.WaitGroup var mu sync.Mutex start := time.Now() for i := 0; i < 100000; i++ { wg.Add(1) go func() { // counterをロック mu.Lock() defer wg.Done() for c := 0; c < 10000; c++ { counter++ } // Counterをアンロック mu.Unlock() }() } wg.Wait() end := time.Now() fmt.Println("counter: ", counter) fmt.Printf("%f秒\n", (end.Sub(start)).Seconds()) } |
couter変数をカウントアップするfor文の前にMutexでロックをかけました。これで排他制御が完了です!
プログラムを実行してみましょう。
1 2 3 4 5 6 7 8 9 10 11 |
$ go run main.go counter: 1000000000 1.726820秒 $ go run main.go counter: 1000000000 1.757983秒 $ go run main.go counter: 1000000000 1.781457秒 |
今度は、counterの値がブレてないですね!
対策2:Atomicを使う
Mutexより手軽に排他制御を行う方法があります。それが、sync.Atomicです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
package main import ( "fmt" "runtime" "sync" "sync/atomic" "time" ) func main() { runtime.GOMAXPROCS(4) var counter uint64 var wg sync.WaitGroup start := time.Now() for i := 0; i < 100000; i++ { wg.Add(1) go func() { defer wg.Done() for c := 0; c < 10000; c++ { atomic.AddUint64(&counter, 1) } }() } wg.Wait() end := time.Now() fmt.Println("counter: ", counter) fmt.Printf("%f秒\n", (end.Sub(start)).Seconds()) } |
先ほどと違って、「atomic.AddUint64(&counter, 1)」のみで排他制御を行いました。
プログラムを実行してみます。
1 2 3 4 5 6 7 8 9 10 11 |
$ go run main.go counter: 1000000000 18.114072秒 $ go run main.go counter: 1000000000 17.928183秒 $ go run main.go counter: 1000000000 19.473888秒 |
今度も問題なくカウントできているみたいですね。しかし、こっちの方がかなり時間がかかるな。。
他のブログや資料を見るとatomicの方が処理時間が早いみたいなんですが、私の環境だとAtomicがめちゃくちゃ時間かかってますね。。
おわりに
MutexやAtomicを使うと排他制御が楽です!ガンガン使っていきましょう!!
それではまた!
最近のコメント