channel以外の方法で異なるgoroutineがアクセスしても、
競合によるエラーが起きないようにするための機能です。
channelではなく、1つのmapに対して異なる2つのgoroutineがアクセスする
次のコードは、エラーが発生することがあります。
func main() {
c := make(map[string]int)
go func() {
for i := 0; i < 10; i++ {
c["key"]++ // 2つのgoroutineで同じmapの同じkeyの値にアクセス
}
}()
go func() {
for i := 0; i < 10; i++ {
c["key"]++ // 2つのgoroutineで同じmapの同じkeyの値にアクセス
}
}()
time.Sleep(1 * time.Second) // 簡易テストのためタイマーを利用
fmt.Println(c, c["key"])
}
2つのgoroutineで、同じmapの同じkeyの値にアクセスしているため、
タイミングによってエラーが発生します。
この問題を解決するのがsync.Mutexです。
今回は、structの1要素としてsync.Mutex型を定義し、
それを使って、競合が起きうる処理の前後でロック・アンロックします。
なお、type structのメソッドでなくても、変数にsync.Mutexを定義し、関数の引数とすることで、
ロック、アンロックを使用できます。
type Counter struct {
v map[string]int
mux sync.Mutex // sync.Mutexを定義
}
func (c *Counter) Write(key string) { // typeに対するメソッド(sync.Mutexはポインタ渡しにすべきなので、ポインタレシーバー)
c.mux.Lock() // 処理前にロック
defer c.mux.Unlock() // 処理後にはアンロック
c.v[key]++
}
func (c *Counter) Read(key string) int { // typeに対するメソッド(sync.Mutexはポインタ渡しにすべきなので、ポインタレシーバー)
c.mux.Lock() // 処理前にロック
defer c.mux.Unlock() // 処理後にはアンロック
return c.v[key]
}
func main() {
c := Counter{v: make(map[string]int)} // Counter型として、vにmakeしたmapを定義
go func() {
for i := 0; i < 10; i++ {
c.Write("key") // 処理ごとにロック・アンロックするため、競合エラーが発生しない
}
}()
go func() {
for i := 0; i < 10; i++ {
c.Write("key") // 処理ごとにロック・アンロックするため、競合エラーが発生しない
}
}()
time.Sleep(1 * time.Second)
fmt.Println(c.v, c.Read("key")) // c.muxを出力すると、値コピーの警告が発生する。&c.muxとすれば解消する
}
1つ注意点です。
sync.Mutexは、値渡しにすると、各goroutine内でコピーが作成されて、
コピーに対してロック・アンロックが処理されてしまいます。
これは修正前と同じ、競合エラーを引き起こす可能性を残してしまうということです。
そのため、上記コードでは、typeで定義したstructに対してメソッドを使っているので、
ポインタレシーバーである必要があります。
type定義したメソッドでない、関数の場合は、引数にsync.Mutexを渡すようにします。