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 が適している場面

複数の変数を同時に更新する必要がある場合。複雑な条件分岐を含む場合。コードの可読性を優先したい場合。

atomic は高速ですが、使い方を誤るとバグの原因になります。迷ったら Mutex を使うのが安全です。