今更ながらData Race Detectorを読んで理解を深めた話

先週の土曜日にGo Conference 2024に参加してきました。

普段から Go を書いている人間からすると勉強になることが多く

とても刺激になる一日でした。

さて、その中の一つのData Race Detection In Go From Beginners Eyeについてを

聞いていて、Data Race Datectorを読んでないなと思い今回自分なりに解釈して読んでみようと思いました。

Data Race Datector

Go で実装していると goroutine を実装する時があると思います。

サクッと実装すると以下のような形です。

package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}

しかし、単純に1つの goroutine ではなく2つ、 3つと実装したい時があります。

その場合、データ競合が起きる場合同時にアクセスし、

どちらかが1つにアクセスがある場合にエラーが発生します。

以下は例です。

func main() {
c := make(chan bool)
m := make(map[string]string)
go func() {
m["1"] = "a" // 最初の競合アクセス。
c <- true
}()
m["2"] = "b" // 2番目の競合アクセス。
<-c
for k, v := range m {
fmt.Println(k, v)
}
}

ただ、見た目では判断することが難しいです。

Goではgo run -raceのフラグがあります。

実行すると以下のように、warningと表示されます

Terminal window
go run -race main.go
==================
WARNING: DATA RACE
Write at 0x00c0001160c0 by goroutine 6:
runtime.mapaccess2_faststr()
/usr/local/go/src/runtime/map_faststr.go:108 +0x42c
main.main.func1()
/go/src/github.com/komisan19/sandbox/sample-race/main.go:11 +0x48
Previous write at 0x00c0001160c0 by main goroutine:
runtime.mapaccess2_faststr()
/usr/local/go/src/runtime/map_faststr.go:108 +0x42c
main.main()
/go/src/github.com/komisan19/sandbox/sample-race/main.go:14 +0xfc
Goroutine 6 (running) created at:
main.main()
/go/src/github.com/komisan19/sandbox/sample-race/main.go:10 +0xe0
==================
2 b
1 a
Found 1 data race(s)
exit status 66

このように競合が発生する場合があります。

以下の場合でもデータ競合が発生します

m := make(map[string]int)
go func() {
for i := 0; i < 1000; i++ {
m[strconv.Itoa(i)] = i // write
}
}()
go func() {
for i := 0; i < 1000; i++ {
fmt.Println(i, m[strconv.Itoa(i)]) // read
}
}()
time.Sleep(time.Second * 5)

このように、Goではデータ競合を簡単に抽出することができます。

しかし、抽出コストがメモリ使用量5-10倍、実行時間が2-20倍になることがありあmす。 (プログラムによる)

個人的な使い方としては、テスト実行時に留めておくのがいいかもしれませんね