体はドクペで出来ている

インフラ、Goの割合が多い技術ブログ

GolangでMutexを使いRace Conditionを回避する

Race Conditionとは

日本語に訳すと「競合状態」でプログラミングの文脈においては「ある処理が並行に動作している別の処理に影響を及ぼし予測不能な結果を引き起こす」ことをいいます(以下RC)。

RCを引き起こす典型的なコード

package main

import (
    "fmt"
    "sync"
)

func main() {
    var count int

    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            count++
        }()
    }
    wg.Wait()

    fmt.Printf("count: %d\n", count)
}

 ※いつもGo Playgroundのリンクを貼っていましたが、何度やっても想定する結果にならなかったので今回は無しとしました

このコードは変数 count をgoroutineで並行に10,000回加算するものです。通常の順次処理であれば count の値は10,000となるはずですが、このコードでの結果は毎回バラバラになるかと思います(実行環境にもよりますが)。

$ go run main.go
count: 8079
$ go run main.go
count: 7984
$ go run main.go
count: 7929

インクリメントはGolangのコードとしては一行であってもコンピューターの動作としては「メモリから現在値の取得」「取得した値をインクリメント」「インクリメントした値をメモリに書き戻す」という3ステップが必要になり、これが並行に動作するためこのような事象が発生します。

f:id:ryo-yamaoka:20190328021024p:plain
RC発生時のシーケンス

MutexによるRC回避

前回の記事でも解説しましたが、Mutexを使うと同時に変数へアクセスできるgoroutineを制限し(この制限区間クリティカルセクションという)RCを回避することができます。

MutexによりRCを回避するコード

package main

import (
    "fmt"
    "sync"
)

func main() {
    var count int
    var mu sync.Mutex

    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock() // クリティカルセクション開始
            count++
            mu.Unlock() // クリティカルセクション終了
        }()
    }
    wg.Wait()

    fmt.Printf("count: %d\n", count)
}

$ go run main.go
count: 10000
$ go run main.go
count: 10000
$ go run main.go
count: 10000

Mutex利用時の注意

MutexによりRCを回避することはできますが、見方を変えると並行に動作している他のgoroutineの動作を妨害しているともいえます。必要以上に広いクリティカルセクションを設けることはパフォーマンスに悪影響を与えることがあるので必要最小限を心がけましょう。

またアンロック忘れは即デッドロックを招くためロック直後に defer mu.Unlock() のようにしておくと安全でしょう(但しこの場合は関数終了までアンロックされないためクリティカルセクションが拡大する)。

そして忘れてはならないのが、Golangには「メモリ共有で通信するな。通信によってメモリを共有しろ」というモットーがありまずはChannelを使って解決できないかを検討すべきです(Channelについては別の機会に)。