技術向上

プログラミングの学び、気になるテクノロジーやビジネストレンドを発信

sync.Mutex【Go】

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を渡すようにします。

tsujitaku50.hatenablog.com

golangbot.com