Go言語のゴルーチンの処理について、mutexを扱います。
複数のゴルーチンが実行されている時に処理が衝突する可能性があります。mutexを使用すると、1つのゴルーチンだけが処理コードにアクセスできるようにロックして、処理の衝突を防ぐことができます。
mutexの処理を見ていきましょう。
複数のgoroutineの処理の衝突
mutexの処理に行く前に、ゴルーチンの処理が衝突するような処理を見てみましょう。
次のコードを見てみましょう。
package main
import (
"fmt"
"time"
)
func main() {
c := make(map[string]int)
go func() {
for i := 0; i < 5; i++ {
c["somekey"] += 1
}
}()
go func() {
for i := 0; i < 5; i++ {
c["somekey"] += 1
}
}()
time.Sleep(time.Second)
fmt.Println(c, c["somekey"])
}
mapを使ってキーを持った値を作ります。
for文を使って、0から4までの値を”somekey”というキーに1づつ増やして順に加えた値を対応させてmapを作る処理をしています。
この処理を関数にして、ゴルーチンで処理をしています。同じ処理をさせるために2つ実行しています。
実行するとこうなります。
map[somekey:10] 10
うまく実行できています。同じmapのキーを扱っているので、2つの処理があわさって計算されているのがわかります。
これは、2つのゴルーチンの処理も衝突せずに上手く行ってるように見えます。
では、この処理のループを0から4までから0から10までに変えたらどうでしょう?
やってみると次のどちらかになると思います。
処理が成功している場合は出力できています。
map[somekey:20] 20
失敗した場合はエラーが表示されます。
fatal error: concurrent map writes
何回か実行すると、上手く行ったり行かなかったりが確認できると思います。
これを、1000までの処理とするとどうでしょうか?
package main
import (
"fmt"
"time"
)
func main() {
c := make(map[string]int)
go func() {
for i := 0; i < 1000; i++ {
c["somekey"] += 1
}
}()
go func() {
for i := 0; i < 1000; i++ {
c["somekey"] += 1
}
}()
time.Sleep(time.Second)
fmt.Println(c, c["somekey"])
}
これは、何回やってもエラーになると思います。極稀に、「map[somekey:2000] 2000」と出力されるかも知れませんが、ほとんどエラーが返されると思います。
二つの処理が同じ結果をmapに入れようとするためにこういうことが起こってしまいます。
これを回避するためにmutexを使います。
sync.Mutex
では、上の処理をmutexを使って書き換えてみます。
sync.MutexのLock()、Unlock()を使って、処理の衝突を防ぐために一度に1つのゴルーチンがアクセスできるようにします。
package main
import (
"fmt"
"sync"
"time"
)
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
c.v[key]++
c.mux.Unlock()
}
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
defer c.mux.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
go func() {
for i := 0; i < 1000; i++ {
c.Inc("somekey")
}
}()
go func() {
for i := 0; i < 1000; i++ {
c.Inc("somekey")
}
}()
time.Sleep(time.Second)
fmt.Println(c, c.Value("somekey"))
}
mapとmutexを持ったSafeCounter型を作ります。
Inc()関数を定義してメソッドを作って、指定したキーの値をひとつ増やす処理をします。
一度に1つのゴルーチンだけがc.vのマップにアクセスできるようにc.mux.Lock()を使ってロックします。そして、マップの値をインクリメントしたあとに、c.mux.Unlock()でロックを解除します。
Value()関数を定義してメソッド作って、指定されたキーのカウンターの値を返す処理をします。この時、先ほどと同様に、一度に1つのゴルーチンだけがc.vのマップにアクセスできるよう処理のロックとロック解除をおこなっています。ロックの解除については、ここではdeferを使いました。
main()関数の中で、SafeCounter型のマップを初期化しています。
そして2つの同じ処理の関数をゴルーチンで実行しています。0から1000までのループ処理で、ここではInc()関数を利用してインクリメントしています。
メソッドを呼び出して実行するとこうなります。
{map[somekey:2000] {0 0}} 2000
今度は、何度繰り返しても衝突が起きないために、エラーにならずに結果が表示されるはずです。
最後に
ここでは、Go言語のゴルーチンの処理についてmutexを扱いました。
複数のゴルーチンが実行されている時に同じ処理が衝突することがあります。mutexを使用すると、1つのゴルーチンだけが処理コードにアクセスできるようにロックして、処理の衝突を防ぐことができます。
sync.MutexのLock()、Unlock()を使って処理をします。