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するメソッドを呼ぶっていうお決まりのパターンなのであれば、静的解析で検出できるかも・・・?とか思った。