Mutex で共有データを安全に扱う

複数の goroutine から同じ変数を読み書きすると、データ競合(data race)が起きることがあります。これを防ぐのが sync.Mutex です。

データ競合とは

次のコードには問題があります。

package main

import (
    "fmt"
    "sync"
)

func main() {
    counter := 0
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++
        }()
    }

    wg.Wait()
    fmt.Println(counter) // 1000 にならないことがある
}

counter++ は「読み取り → 加算 → 書き込み」の 3 ステップから成ります。複数の goroutine がこれを同時に行うと、お互いの変更を上書きしてしまいます。

Mutex で排他制御する

Mutex は「ロック」を提供します。ロックを取得した goroutine だけが処理を進められ、他は待機します。

package main

import (
    "fmt"
    "sync"
)

func main() {
    counter := 0
    var mu sync.Mutex
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            counter++
            mu.Unlock()
        }()
    }

    wg.Wait()
    fmt.Println(counter) // 必ず 1000 になる
}

Lock() でロックを取得し、Unlock() で解放します。Lock と Unlock の間は、一度に 1 つの goroutine しか実行できません。

defer で Unlock する

Unlock を忘れるとデッドロックになります。defer を使うと安全です。

mu.Lock()
defer mu.Unlock()
// 処理

途中で return や panic が起きても、defer なら確実に Unlock されます。

構造体に埋め込む

Mutex を構造体に埋め込むパターンはよく使われます。

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

データとロックをセットにすることで、安全なアクセスをカプセル化できます。

RWMutex で読み取りを並行化

sync.RWMutex は読み取りと書き込みを区別します。

Mutex

読み取りも書き込みも、一度に 1 つの goroutine しかアクセスできない。

RWMutex

書き込みは 1 つだけ。読み取りは複数同時に可能。

読み取りが多く書き込みが少ない場合、RWMutex の方が効率的です。

var rw sync.RWMutex

// 読み取り
rw.RLock()
defer rw.RUnlock()
// 読み取り処理

// 書き込み
rw.Lock()
defer rw.Unlock()
// 書き込み処理

読み取りには RLock() / RUnlock()、書き込みには Lock() / Unlock() を使います。

Mutex と channel の使い分け

Go のことわざに「共有メモリで通信するな、通信でメモリを共有せよ」というものがあります。channel でデータを渡す方が Go らしいスタイルとされています。

ただし、単純なカウンタやキャッシュなど、共有状態を保護するだけなら Mutex の方がシンプルなこともあります。状況に応じて使い分けてください。