グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



goroutineでmapにアクセスするときは排他制御をしよう

Goではgoroutineを使って簡単に並行処理を書けますが、何気ないところでエラーが発生してしまうことが(筆者は)度々あります。
さらに偶然にも正常に処理が完了し、何回かに1回しか失敗しないケースの場合、なかなか気付かないこともあります。

今回はmapの競合で発生したエラーと、エラーを起こさないための排他制御の使い方を見ていこうと思います。

エラーが発生する原因

まずは意図的にエラーを発生させてみます。

goroutineの外で定義したmapに、複数のgoroutineから書き込みを行います。

func main() {
	m := map[string]string{}

	for i := 0; i < 1000; i++ {
		go func() {
			m["key"] = "value"
		}()
	}

	time.Sleep(time.Second * 3)
	fmt.Println(m)
}

正常に終了することもありますが、エラーが発生した場合の出力結果は以下の通りです。

fatal error: concurrent map writes

goroutine 811 [running]:
runtime.throw({0x497484, 0x0})
	/usr/local/go-faketime/src/runtime/panic.go:1198 +0x71 fp=0xc0001a0748 sp=0xc0001a0718 pc=0x42f791
runtime.mapassign_faststr(0x488840, 0xc00010c180, {0x494907, 0x3})
	/usr/local/go-faketime/src/runtime/map_faststr.go:294 +0x38b fp=0xc0001a07b0 sp=0xc0001a0748 pc=0x40fd4b
main.main.func1()
...

1つのgoroutineがmapに書き込んでいる場合、他のgoroutineが同時に読み書きすることができないので、panicが発生しました。

Go1.6のリリースノートにも以下のような記述がありました。

if one goroutine is writing to a map, no other goroutine should be reading or writing the map concurrently. If the runtime detects this condition, it prints a diagnosis and crashes the program.Go 1.6 Release Notes

sync.Mutexで排他制御をかける

func main() {
	var mu sync.Mutex

	m := map[string]string{}

	for i := 0; i < 1000; i++ {
		go func() {
			mu.Lock()
			defer mu.Unlock()

			m["key"] = "value"
		}()
	}

	time.Sleep(time.Second * 3)
	fmt.Println(m) // map[key:value]
}

先ほどエラーが発生したコードに排他制御を追加したところ、今度は何回実行しても正常に処理が終了します。

mapにアクセスする前にロックを取得するため、競合することが無くなりました。

sync.Mapを使ってみる

先ほどはsync.Mutexを使って自分で排他制御をしましたが、sync.Mapを使えば自分で排他制御を書かなくても競合しないので、手軽に使用できそうです。

まずはMapの構造体と、値を追加するStoreメソッドの実装を覗いてみましょう。(一部省略)

// Map is like a Go map[interface{}]interface{} but is safe for concurrent use
// by multiple goroutines without additional locking or coordination.
// Loads, stores, and deletes run in amortized constant time.
...
type Map struct {
	mu      Mutex
	read    atomic.Value // readOnly
	dirty   map[interface{}]*entry
	misses  int
}

// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}

	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
        ...
	}
	m.mu.Unlock()
}

この中で Lock/Unlock が行われているので、メソッドを使う側ではそれを意識しなくて済むようになっています。

それでは、sync.Mapを実際に使ってみたいと思います。

func main() {
	m := sync.Map{}

	for i := 0; i < 1000; i++ {
		go func() {
			m.Store("key", "value")
		}()
	}

	time.Sleep(time.Second * 3)

	if v, ok := m.Load("key"); ok {
		fmt.Println(v) // value
	}
}

排他制御を自分で書かなくても、並行処理でmapへ書き込みができました。

ロックを意識せず直感的に使用できる点では非常に便利ですが、key,valueに型の制約がないため、多少の使いずらさを感じました。

まとめ

今回はgoroutineで同一のmapにアクセスする方法を検証しました。

方法はいくつかありましたが、並行処理で同一のmapや変数にアクセスする場合はいずれかの方法で排他制御が必要になります。
偶然処理が正常に完了してしまうこともあるので、goroutineを使用する際は十分に検証する事が大切かと思います。

また、今回紹介していませんがsync.RWMutex では読み取り用のロックもあるので、実装に応じて検討してみてください。

この記事を書いた人

アーティス
アーティス
創造性を最大限に発揮するとともに、インターネットに代表されるITを活用し、みんなの生活が便利で、豊かで、楽しいものになるようなサービスやコンテンツを考え、創り出し提供しています。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします