Go の atomic パッケージでロックなしに値を操作する
Mutex によるロックは安全ですが、オーバーヘッドがあります。単純なカウンタの増減など、基本的な操作だけならロックなしで安全に行える方法があります。それが sync/atomic パッケージです。
atomic とは
atomic(アトミック)操作は、CPU レベルで「分割不可能」な操作を保証します。他の goroutine から見ると、操作は一瞬で完了したように見え、途中の状態は観測されません。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1)
}()
}
wg.Wait()
fmt.Println("カウント:", counter) // 必ず 1000
}atomic.AddInt64 を使えば、Mutex なしで安全にカウンタを増減できます。
主要な関数
atomic パッケージには型ごとにいくつかの関数が用意されています。
| 関数 | 説明 | 対応型 |
|---|---|---|
| AddInt64 | 値を加算 | int64, int32, uint64, uint32 |
| LoadInt64 | 値を読み取り | int64, int32, uint64, uint32 |
| StoreInt64 | 値を書き込み | int64, int32, uint64, uint32 |
| SwapInt64 | 値を交換 | int64, int32, uint64, uint32 |
それぞれの関数には Int32, Int64, Uint32, Uint64 版があります。ポインタ用の LoadPointer, StorePointer なども存在します。
Load と Store
単純な読み書きにも atomic が必要な場合があります。
var flag int32
func setFlag() {
atomic.StoreInt32(&flag, 1)
}
func checkFlag() bool {
return atomic.LoadInt32(&flag) == 1
}
func main() {
go setFlag()
for !checkFlag() {
// フラグが立つまで待機
}
fmt.Println("フラグが立ちました")
}通常の代入や読み取りは、コンパイラの最適化や CPU のキャッシュの影響で、他の goroutine から見えないことがあります。atomic を使うことで、確実に最新の値を読み書きできます。
CompareAndSwap(CAS)
CompareAndSwap は「現在の値が期待値と一致したら、新しい値に置き換える」という操作をアトミックに行います。
func main() {
var value int64 = 10
// value が 10 なら 20 に変更
swapped := atomic.CompareAndSwapInt64(&value, 10, 20)
fmt.Println("交換成功:", swapped) // true
fmt.Println("値:", value) // 20
// value が 10 なら 30 に変更(今は 20 なので失敗)
swapped = atomic.CompareAndSwapInt64(&value, 10, 30)
fmt.Println("交換成功:", swapped) // false
fmt.Println("値:", value) // 20(変わらず)
}CAS はロックフリーなデータ構造を実装する基本的な部品です。失敗したらリトライするパターンがよく使われます。
スピンロックの実装
CAS を使って簡易的なスピンロックを実装できます。
type SpinLock struct {
locked int32
}
func (s *SpinLock) Lock() {
for !atomic.CompareAndSwapInt32(&s.locked, 0, 1) {
// ビジーウェイト
}
}
func (s *SpinLock) Unlock() {
atomic.StoreInt32(&s.locked, 0)
}ただし、実際のプロダクションコードでは sync.Mutex を使うべきです。スピンロックは CPU を消費し続けるため、待機時間が長い場合は非効率です。
atomic.Value
任意の型の値をアトミックに読み書きするには atomic.Value を使います。
func main() {
var config atomic.Value
// 設定を保存
config.Store(map[string]string{
"host": "localhost",
"port": "8080",
})
// 設定を読み取り
cfg := config.Load().(map[string]string)
fmt.Println(cfg["host"]) // localhost
}設定のホットリロードなど、複数の goroutine から参照される設定値の更新に便利です。
いつ atomic を使うべきか
atomic と Mutex の使い分けは以下を参考にしてください。
単純なカウンタ、フラグ、設定値の読み書き。操作が単一の変数に対する単純なものに限定される場合。
複数の変数を同時に更新する必要がある場合。複雑な条件分岐を含む場合。コードの可読性を優先したい場合。
atomic は高速ですが、使い方を誤るとバグの原因になります。迷ったら Mutex を使うのが安全です。