🏃‍♂️

Go の Mutex と -race オプション

2024/08/23に公開

まずはこのコードを実行してみます。

package main

import (
	"fmt"
	"sync"
)

func main() {
	var a []int
	var wg sync.WaitGroup

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			a = append(a, i)
		}(i)
	}

	wg.Wait()
	fmt.Println(len(a))
}

このコードでは a というスライスに1,000個の要素を追加しています。なので実行すると 1000 が出力されるはずですが、実際には 949 とか 933 といった数字になります。

これはforループ内のgoルーチンが並列に実行されるからですね。

こういったスレッドアンセーフなコードを検出するためのツールが、Goでは標準で用意されています。
以下のように、-race をつけて実行してみましょう。

$ go run -race  mutex-test.go
==================
WARNING: DATA RACE
Read at 0x00c000110150 by goroutine 7:
....
Previous write at 0x00c000110150 by goroutine 6:
....

このようにレースコンディションになった箇所を表示してくれます。

原因がわかったので、修正してみます。
といってもMutexで該当箇所を保護するだけです。

func main() {
	var a []int
	var mutex = new(sync.Mutex) // 追加
	var wg sync.WaitGroup

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			mutex.Lock() // 追加
			a = append(a, i)
			mutex.Unlock() // 追加
		}(i)
	}

	wg.Wait()
	fmt.Println(len(a))
}

これを実行すると、期待した 1000 が出力されます。
-race をつけて実行しても、先ほどのワーニングは出力されません。

この種のバグは見つけにくいのですが、Goだと簡単に切り分けができ、修正の確認もしやすのでとても便利ですね。

Discussion