LoginSignup
8
1

GoでRace Conditionを発見する

Last updated at Posted at 2020-02-26

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 testgo 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

参考資料

8
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1