goroutine を使う選択をしたときに、あなたはRace Conditionに注意しなければならない。
countを5000回加算するコード(直列処理)
main.go
package main
import (
"fmt"
)
func main() {
var count int
for i := 0; i < 5000; i++ {
count++
}
fmt.Printf("count: %d\n", count)
}
結果
$ go run ./main.go
count: 5000
愚直に並行処理にする
この変更はRace Conditionを引き起こし問題になる
import (
"fmt"
+ "sync"
)
func main() {
var count int
+ var wg sync.WaitGroup
for i := 0; i < 5000; i++ {
- count++
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ count++
+ }()
}
+ wg.Wait()
fmt.Printf("count: %d\n", count)
}
Bad: countを5000回加算するコード(並行処理)
これではいけない
package main
import (
"fmt"
"sync"
)
func main() {
var count int
var wg sync.WaitGroup
for i := 0; i < 5000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
count++
}()
}
wg.Wait()
fmt.Printf("count: %d\n", count)
}
結果
5000よりも少なくなる。 それぞれの goroutine が count を読んだタイミングが異なるため。
$ go run ./main.go
count: 4354
Race Conditionを発見する -race
オプション
go run -race main.go
のように -race
オプションを付けるとRace Conditionを検出できる。このオプションは、 go test
や go build
でも有効。
-race
をつければRace Conditionを検出し、レポートを出力することができるが、メモリ使用量が増したり、実行時間が増えたりするので注意が必要。ベンチマークテストや負荷試験をするときに、CIで回すことが推奨される。
次のようにどこで Race Condition が起きたかレポートしてくれる。
$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c0000ac008 by goroutine 8:
main.main.func1()
/Users/hogehoge/workspace/go-test/main.go:16 +0x6c
Previous write at 0x00c0000ac008 by goroutine 7:
main.main.func1()
/Users/hogehoge/workspace/go-test/main.go:16 +0x82
Goroutine 8 (running) created at:
main.main()
/Users/hogehoge/workspace/go-test/main.go:14 +0xe7
Goroutine 7 (finished) created at:
main.main()
/Users/hogehoge/workspace/go-test/main.go:14 +0xe7
==================
count: 5000
Found 1 data race(s)
exit status 66
排他制御を導入してRace Conditionが起こらないように変更する
func main() {
var count int
+ var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 5000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
+ mu.Lock()
+ defer mu.Unlock()
count++
}()
}
Good: countを5000回加算するコード(並行処理)
main.go
package main
import (
"fmt"
"sync"
)
func main() {
var count int
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 5000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
count++
}()
}
wg.Wait()
fmt.Printf("count: %d\n", count)
}
結果
$ go run ./main5.go
count: 5000