GolangでMutexを使いRace Conditionを回避する
Race Conditionとは
日本語に訳すと「競合状態」でプログラミングの文脈においては「ある処理が並行に動作している別の処理に影響を及ぼし予測不能な結果を引き起こす」ことをいいます(以下RC)。
RCを引き起こす典型的なコード
package main import ( "fmt" "sync" ) func main() { var count int var wg sync.WaitGroup for i := 0; i < 10000; i++ { wg.Add(1) go func() { defer wg.Done() count++ }() } wg.Wait() fmt.Printf("count: %d\n", count) }
※いつもGo Playgroundのリンクを貼っていましたが、何度やっても想定する結果にならなかったので今回は無しとしました
このコードは変数 count
をgoroutineで並行に10,000回加算するものです。通常の順次処理であれば count
の値は10,000となるはずですが、このコードでの結果は毎回バラバラになるかと思います(実行環境にもよりますが)。
$ go run main.go count: 8079 $ go run main.go count: 7984 $ go run main.go count: 7929
インクリメントはGolangのコードとしては一行であってもコンピューターの動作としては「メモリから現在値の取得」「取得した値をインクリメント」「インクリメントした値をメモリに書き戻す」という3ステップが必要になり、これが並行に動作するためこのような事象が発生します。
MutexによるRC回避
前回の記事でも解説しましたが、Mutexを使うと同時に変数へアクセスできるgoroutineを制限し(この制限区間をクリティカルセクションという)RCを回避することができます。
MutexによりRCを回避するコード
package main import ( "fmt" "sync" ) func main() { var count int var mu sync.Mutex var wg sync.WaitGroup for i := 0; i < 10000; i++ { wg.Add(1) go func() { defer wg.Done() mu.Lock() // クリティカルセクション開始 count++ mu.Unlock() // クリティカルセクション終了 }() } wg.Wait() fmt.Printf("count: %d\n", count) }
$ go run main.go count: 10000 $ go run main.go count: 10000 $ go run main.go count: 10000
Mutex利用時の注意
MutexによりRCを回避することはできますが、見方を変えると並行に動作している他のgoroutineの動作を妨害しているともいえます。必要以上に広いクリティカルセクションを設けることはパフォーマンスに悪影響を与えることがあるので必要最小限を心がけましょう。
またアンロック忘れは即デッドロックを招くためロック直後に defer mu.Unlock()
のようにしておくと安全でしょう(但しこの場合は関数終了までアンロックされないためクリティカルセクションが拡大する)。
そして忘れてはならないのが、Golangには「メモリ共有で通信するな。通信によってメモリを共有しろ」というモットーがありまずはChannelを使って解決できないかを検討すべきです(Channelについては別の機会に)。