TL;DR
2 つの別々のスレッドが、同じメモリアドレスに次の条件でアクセスすること。
- 少なくとも 1 つのアクセスが書き込みである
- アクセスの順序を強制する同期の仕組みがない
なぜ必要か
The Go Memory Model によると、データ競合のないプログラムは、すべての ゴルーチンが 1 つのプロセッサの上で切り替わりながら動いているかのように、逐次一貫性を持って実行される。この性質は DRF-SC(data-race-free programs execute in a sequentially consistent manner)と呼ばれる。逆に、データ競合のあるプログラムは正しくなく、逐次一貫性のない実行結果になりうる。
仕組み
The Go Memory Model では、データ競合を「あるメモリ位置への書き込みが、同じ位置への別の読み込みか書き込みと並行して起こること」と定義している。ただし、関係するアクセスがすべて sync/atomic パッケージによるアトミックなアクセスであれば、データ競合にはならない。
データ競合があると、ある読み込みは、並行して実行されたどの書き込みの値でも観測しうる。さらに、インタフェース値、マップ、スライス、文字列のように複数ワードからなる値で競合が起きると、1 回の書き込みに対応しない不整合な値が読まれ、任意のメモリ破壊につながることがある。
例
The Go Memory Model には次の例がある。
var a, b int
func f() {
a = 1
b = 2
}
func g() {
print(b)
print(a)
}
func main() {
go f()
g()
}f と g が同期なしに a と b にアクセスしているので、g が 2 を表示したあとに 0 を表示することがありうる。
注意点
複数のゴルーチンが同時にアクセスするデータを変更するときは、アクセスを直列化する必要がある。The Go Memory Model は、チャネルの操作や、sync・sync/atomic パッケージの同期プリミティブでデータを守るよう勧めている。Go では go build -race で ThreadSanitizer を使った競合検出を有効にでき、データ競合を検出するとそれを報告してプログラムを止める。