TL;DR
分割できない(アトミックな)単一の操作。Go では sync/atomic パッケージにまとまっている。
なぜ必要か
mutex はプログラムの一部を逐次処理にしてしまうので、整数などの単純な変数の値を更新するだけなら、mutex を使うより効率よく処理できる。
例
Go で、通常の加算とアトミック操作による加算を比べたベンチマーク。
var total = int64(0)
func BenchmarkNormal(bench *testing.B) {
for range bench.N {
total += 1
}
}
func BenchmarkAtomic(bench *testing.B) {
for range bench.N {
atomic.AddInt64(&total, 1)
}
}$ go test -bench=. -count 3
goos: darwin
goarch: arm64
pkg: listing12_3
cpu: Apple M2 Max
BenchmarkNormal-12 538361530 1.987 ns/op
BenchmarkNormal-12 606458019 1.965 ns/op
BenchmarkNormal-12 603949323 1.975 ns/op
BenchmarkAtomic-12 308510485 3.907 ns/op
BenchmarkAtomic-12 303942386 3.967 ns/op
BenchmarkAtomic-12 307332045 3.837 ns/op
PASS
ok listing12_3 9.182sアトミック操作は、通常の加算のおよそ 2 倍の時間がかかっている。
注意点
アトミック操作では、複数の変数を同時に更新できない。
また、アトミック操作ではコンパイラや CPU の最適化が効かなくなるので、通常の方法で変数を更新するほうが、アトミック操作よりはるかに速い。
たとえば CPU は、よくアクセスする変数をキャッシュに置いて処理を速くする。キャッシュの容量には限りがあるので、必要に応じて変数の内容をメインメモリに書き戻す(フラッシュする)。一方アトミック操作では、並列に動く他のスレッドが、変数の更新を確実に確認できなければならない。そのため CPU はキャッシュの一貫性(キャッシュコヒーレンシ)を保つ必要があり、メインメモリに書き戻したり、他の CPU がキャッシュしているデータを無効にしたりする。