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 は読み取りと書き込みを区別します。
読み取りも書き込みも、一度に 1 つの goroutine しかアクセスできない。
書き込みは 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 の方がシンプルなこともあります。状況に応じて使い分けてください。