sync.Mutexを使って初歩的なミスで詰まった思い出

Goでコードを書いてて、排他制御のためsync.Mutexを使っていた。

テストしてると、ある時点でロック獲得待ちのままずっと止まってしまい「あれ・・・???」てなったので覚えてるうちにメモ。

原因:Lock()するメソッドの中でさらにLockするメソッドを呼んでいた

たとえば次の構造体Testで変数 value を排他制御したいとする。 そうなると、valueを触るメソッドではすべて sync.Mutex をつかってロックをかけるのだが、次の例ではうっかり Increment() の中で自身の値をログに出そうとして、直接 value を参照せずに、Value()メソッドを呼んでしまった。すでにLock()済みな時点でValue()を呼び出そうとしても、ロック獲得ができないのでずっと待ち続ける。

package main

import (
    "log"
    "sync"
)

func main() {
    t := &Test{
        value: 0,
        mu:    sync.Mutex{},
    }
    t.Increment()
}

type Test struct {
    value int
    mu sync.Mutex
}

func (t *Test) Value() int {
    t.mu.Lock()
    defer t.mu.Unlock()
    return t.value
}

func (t *Test) Increment() {
    t.mu.Lock()
    defer t.mu.Unlock()
    log.Printf("before: %v", t.Value())
    t.value++
    log.Printf("after: %v", t.Value())
}

直し方は簡単で、Lock()しているメソッド内ではメソッドを呼ぶのではなく、直接値を触るようにすればよい。

- log.Printf("before: %v", t.Value())
+ log.Printf("before: %v", t.value)

- log.Printf("after: %v", t.Value())
+ log.Printf("after: %v", t.value)

こんな初歩的なミス、普通にすぐ気づくのでは・・・?とこの簡単な例だけ見ると思うかもしれないけど、 複雑な構造体になるといろいろな場所を疑ってしまって、これが原因だと気づくまでに1時間ぐらいかかった。

すべてのgoroutineがロック獲得待ちで止まっている場合、Goは次のエラーを出して教えてくれる。

fatal error: all goroutines are asleep - deadlock!

しかし、他に1つでも元気に動いてるgoroutineがいると、このエラーは出ない。 そのため、ある程度複雑なプログラムになるとこのエラーで気づくことはできない。

Lock()するメソッドの中でさらにLockするメソッドを呼ぶっていうお決まりのパターンなのであれば、静的解析で検出できるかも・・・?とか思った。