THINK

Goにおける排他制御の挙動

7/9/2020

はじめに

排他制御とは、 ある資源にアクセスするときに 自分以外が同時にアクセスしないようにする制御のこと

Goでいうと、 syncpackage を利用して次のように実現出来る

var num int

func mutex() {
	var mtx sync.Mutex
	mtx.Lock()
	num++
	mtx.Unlock()
}

ここで疑問に思ったのが、 mtx.Lock()は何を Lock しているのか? ということ

  1. 資源のLockを行うということは、 この時numを Lock しているのか?
  2. もし、numを Lock するのであれば、 どうやって numを Lock しているのか?
  3. sync.Mutexに Scope があって、 その範囲にあるものを Lock しているのか?

確かめてみる

試しにこんなコードを用意してみた

※ 試行錯誤の結果にできた検証用コードなので、かなり賢いコードになってしまっている

func mutex(num *int, wg *sync.WaitGroup, mtx *sync.Mutex) {
        if wg != nil {
                defer func() {
                        wg.Done()
                }()
        }
        if mtx != nil {
                mtx.Lock()
                defer func() {
                        mtx.Unlock()
                }()
        }
        if num != nil {
                *num++
        }
}

まずは、Lockしないコードで確認

func main() {
        var num int
        var wg sync.WaitGroup
        for i := 0; i < 10000; i++ {
                wg.Add(1)
                go mutex(&num, &wg, nil)
        }
        wg.Wait()
        fmt.Println(num)
}
7783

結果は期待通り 10000にはなってくれていない

次にLockしてみる

func main() {
        var num int
        var wg sync.WaitGroup
        var mtx sync.Mutex
        for i := 0; i < 10000; i++ {
                wg.Add(1)
                go mutex(&num, &wg, &mtx)
        }
        wg.Wait()
        fmt.Println(num)
}
10000

ここまで期待通り

もう一つループを増やす...

func main() {
        var num int
        var wg sync.WaitGroup
        var mtx sync.Mutex
        for i := 0; i < 10000; i++ {
                wg.Add(1)
                go mutex(&num, &wg, &mtx)
        }

        for i := 0; i < 10000; i++ {
                wg.Add(1)
                go mutex(&num, &wg, nil)
        }
        wg.Wait()
        fmt.Println(num)
}
18582

ここで、排他制御をしている対象に対して 外から制御を行わないアクセスは可能なことが判明

func main() {
        var num int
        var wg sync.WaitGroup
        var mtx sync.Mutex
        for i := 0; i < 10000; i++ {
                wg.Add(1)
                go mutex(&num, &wg, &mtx)
        }

        for i := 0; i < 10000; i++ {
                wg.Add(1)
                go mutex(&num, &wg, &mtx)
        }
        wg.Wait()
        fmt.Println(num)
}
20000

同じ mutexを渡すと期待通りになる

ここで、与える mutexを変更してみる...

func main() {
        var num int
        var wg sync.WaitGroup
        var mtx sync.Mutex
        for i := 0; i < 10000; i++ {
                wg.Add(1)
                go mutex(&num, &wg, &mtx)
        }


        var mtx2 sync.Mutex
        for i := 0; i < 10000; i++ {
                wg.Add(1)
                go mutex(&num, &wg, &mtx2)
        }
        wg.Wait()
        fmt.Println(num)
}
19997

これが一見うまく行っているように見えたのだが、 2つの mutexが競合していることが判明

推測

これらのことから、 mutexが Lockをしているのは対象の値ではなく mutex自身であると推測

mutexが Lock をしようとしたときに、 すでに Lock されている場合は Unlock されるのをひたすら待つという感じになる

よく例としてglobal変数に対して Lock をして〜というのがあるけど、 これは非常にまずい状態なのではないかなと

本当に排他制御で値を管理するには値を隠蔽して setter and getterを使って その中で必ず排他制御されるようにしないといけないはず

(だから redux なんてものがあるのかとこの時納得)

※あくまで挙動からの勝手な想像なのに注意

真実を知るために我々は密林の奥地へと進むことにした...

余談

このコードは順次処理も出来る (意味があるわけではない)

func main() {
        var num int
        var wg sync.WaitGroup
        var mtx sync.Mutex
        for i := 0; i < 10000; i++ {
                wg.Add(1)
                go mutex(&num, &wg, &mtx)
        }

        mtx2 := &mtx
        for i := 0; i < 10000; i++ {
                wg.Add(1)
                go mutex(&num, &wg, mtx2)
        }
        wg.Wait()

        for i := 0; i < 10000; i++ {
                mutex(&num, nil, nil)
        }
        fmt.Println(num)
}
30000

ちなみに、godocには次のように記載されている

A Mutex must not be copied after first use.