https://qiita.com/po3rin/items/18a6621f39e9a6f6f7c4
More than 3 years have passed since last update.
race
をつけてCIを通しているのにAPIがデータ競合で落ちてしまいました。調べていたらrace
がそもそも何をしているかに行き着いたので簡単に共有します。race
はコンパイラフラッグの一種で競合を検知するのに便利です。例えば下記のコードはmapへの読み書きが同時に起こってパニックするコードです。これをgo run main.go -race
のように実行するとwarningを出してくれます。package main
import (
"fmt"
"strconv"
"time"
)
func main() {
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)
}
下記のように実行するとWarningを出します。
内部ではC/C++
用の競合検出ライブラリが使われています。簡単に言うと、競合を実行時に検出するコードを出力することができるライブラリです。https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual
もちろんGo
のコンパイラはこれをサポートしており、実際にruntime/race
のREADME.md
にはThreadSanitizer
が使われていることが明記されています。
untime/race package contains the data race detector runtime library. It is based on ThreadSanitizer race detector, that is currently a part of the LLVM project (http://llvm.org/git/compiler-rt.git).
コンパイル時に実際にどのようなコードが仕込まれているかを見てみましょう。例えば下記のコードをビルドした場合をみてみます。
普通にビルドすると下記のようにコンパイルされます。
...
pcdata $2, $1
pcdata $0, $1
movq "".x+8(SP), AX
pcdata $2, $0
incq (AX)
...
race
をつけると下記のようにコンパイルされます。...
pcdata $2, $1
movq "".x+32(SP), AX
testb AL, (AX)
pcdata $2, $0
movq AX, (SP)
call runtime.raceread(SB)
pcdata $2, $1
movq "".x+32(SP), AX
movq (AX), CX
movq CX, ""..autotmp_4+8(SP)
pcdata $2, $0
movq AX, (SP)
call runtime.racewrite(SB)
movq ""..autotmp_4+8(SP), AX
incq AX
pcdata $2, $2
pcdata $0, $1
movq "".x+32(SP), CX
...
コンパイラーがcall runtime.raceread(SB)
のように同時に到達可能な各メモリー位置に読み取りおよび書き込みを検知する命令を追加しています。ご覧の通り、-race
をつけると命令が増えてパフォーマンスが落ちるのでビルドしたバイナリを本番に乗っけるのはやめましょう。go build -race でプログラムが遅くなってた話
ちなみにGoコンパイラが吐き出すアセンブリはCompiler Explorer
というサービスで簡単に確認できて便利です。