概要
Goの「無害」なdata raceについてはたしてそれは実際には無害なのか、避けるべきなのか例を交えてまとめている
結論
「無害」なdata raceも避けるべき
そもそもdata raceとは?
二つ以上のスレッドが同じ変数(メモリ)に対して同時にアクセスし、少なくとも一つのスレッドが書き込みの場合に発生する状態のこと
参考:https://ja.wikipedia.org/wiki/%E3%83%87%E3%83%BC%E3%82%BF%E7%AB%B6%E5%90%88
data raceの例
典型的な例で言うと、以下のように二つのgroutineからcount変数に対してアクセスする場合、
以下は0,1,2,3,4,5,,と表示されるわけではない
おそらく(タイミングによって変わる)0,0,0,0,1,,1,1,1,1みたいに表示される
package main
import (
"fmt"
)
func main() {
var count int
ch := make(chan bool)
go func() {
for {
count = count + 1
fmt.Println(count)
}
}()
go func() {
for {
fmt.Println(count)
}
}()
<-ch
}
output
0
0
1
1
1
1
1
1
1
count変数を正確にひとつずつインクリメントしたい場合にはこの挙動はバグである
これが典型的な有害なdata raceである
これを解決するには、Mutex等を使ってcount変数へのアクセスを一つにlockする必要がある(ここでは解説しない)
参考
https://go.dev/tour/concurrency/9
無害なdata raceとは
では、無害なdata raceとはなんなのか?
countの値が100%順番を保っていないでもOKという時に、それは「無害」なdata raceという
先ほどと同じコード例
package main
import (
"fmt"
)
func main() {
var count int
ch := make(chan bool)
go func() {
for {
count = count + 1
fmt.Println(count)
}
}()
go func() {
for {
fmt.Println(count)
}
}()
<-ch
}
では、「無害」なdata raceは放っておいても良いのか?
それとも避けるべきなのか?
Goでは、hashmapのdata raceを避けるために、data raceが検出されるとデフォルトでパニックを行うようになっている
文字列やHashMapのような複雑なデータ構造に対するdata raceは言うまでもなく、危険である(プログラムがクラッシュする可能性がある)>
GoではHashMapに対するdata raceは実行時にパニックを起こすようになっている
↓これを実行すると
package main
import "fmt"
func main() {
ch := make(chan bool)
hashMap := map[string]string{"a": "a"}
go func() {
for {
hashMap["a"] = "aa"
}
}()
go func() {
for {
fmt.Println(hashMap["a"])
}
}()
<-ch
}
↓のようにパニックを引き起こす
fatal error: concurrent map read and map write
goroutine 7 [running]:
main.main.func2()
/tmp/sandbox504299743/prog.go:19 +0x35
created by main.main in goroutine 1
/tmp/sandbox504299743/prog.go:17 +0xf6
goroutine 1 [chan receive]:
main.main()
/tmp/sandbox504299743/prog.go:24 +0x105
goroutine 6 [runnable]:
main.main.func1()
/tmp/sandbox504299743/prog.go:13 +0x45
created by main.main in goroutine 1
/tmp/sandbox504299743/prog.go:11 +0xb6
Program exited.
改めて結論
「無害」なdata raceも避けるべき
参考:Benign Data Races: What Could Possibly Go Wrong?
Goのrace detector作者による「無害な」data raceは何を引き起こすのかをまとめた資料
なぜなら
著者は以下の理由を挙げている
var count int
count++
-
Goにおける↑のようなコードは未定義の動作である
未定義の動作とは実行時に事実上どんな動作でも起こり得ると言うことを指している -
atomicやmutexを利用することで、data raceが発生しているのかどうかがコードで明確になる
-
このような無害なdata raceを残しておくことで、data race detactorが検知する他の重要なdata raceを見逃してしまう可能性がある
「無害」なdata raceがどのようにして悪影響を及ぼす可能性があるのか例
前項の未定義の動作とは、どのような動作が考え得るのか
↓この場合、goroutine1でstop=trueになる前に、goroutine2が終了する可能性がある
var stop bool;
// goroutine1
go func(){
...
stop = true
}()
// goroutine2
go func(){
for {
if stop {
return
}
...
}
}()
コンパイラによっては、race freeである前提で動作をする
変数stopが定義されるとそのメモリ領域を一時的なデータを格納しておく利用をする場合がある
その際に、stopに予期せぬデータが入っている状態でgoroutine2がstop変数にアクセスすると、予期せぬタイミングでgoroutine2が終了する
また、たとえば一時的なデータが非常に危険な関数のポインタとすると、予期せぬタイミングでその関数が実行されることになり、非常に危険な状態!
Go公式ドキュメントにも記載
Goの公式ドキュメントを読むと↓以下のモットーがある
Do not communicate by sharing memory; instead, share memory by communicating.
メモリをシェアすることでgoroutine間でコミュニケーションするのではなく、コミュニケーションをすることでメモリをシェアしよう!