Go並列実行時のmapについて

お疲れ様です。サーバーエンジニアのSです。

今回は業務で体験した、普段は正常に稼働しているのに数回に1回しか発生しないので発覚しづらいエラーについて、原因と解決方法についてご紹介できればと思います。

 

事象と原因

Goのgoroutineなどを使用して並列処理を行っている場合、mapの読み込み中にmapへの書き込み処理が走るとpanicが発生します。

以下を実行すると実際にエラーを発生させる事ができます。

import (
	"fmt"
)

func main() {
	m := map[string]string{}
	fmt.Println("START")
	for i := 0; i < 10000; i++ {
		go func() {
			m["key"] = "value"
		}()
	}
	fmt.Println("END")
}

必ずではないですが、下記エラーが発生する場合があります。

START
fatal error: concurrent map writes

goroutine 641 [running]:
main.main.func1()
	/tmp/sandbox864889489/prog.go:15 +0x30
created by main.main
	/tmp/sandbox864889489/prog.go:14 +0x6e

goroutine 1 [runnable]:
main.main()
	/tmp/sandbox864889489/prog.go:14 +0x8f
...

対応方法

mapへの処理が同時に走らないように排他制御をかけます。
今回はsync.Mapを使って排他制御を行います。
syncパッケージのMapを使用すると、使っているmapを置き換えるだけなのでお手軽です。

import (
	"fmt"
	"sync"
)

func main() {
	m := sync.Map{}
	fmt.Println("START")
	for i := 0; i < 100000; i++ {
		go func() {
			m.Store("key", "value")
		}()
	}
	fmt.Println("END")
}
START
END

エラーが発生しなくなりました。

まとめ

Goのリファレンスにも記載がありますが、並列処理で同一のmapにアクセスする場合は排他制御が必要となります。

排他制御をしていなくても偶然に処理が正常終了し、発見が遅れる事があるので注意が必要です。

今回はsync.Mapを使用しましたが、他にも対応方法はあるので実装に応じて検討してみて下さい。

ココネでは一緒に働く仲間を募集中です。

ご興味のある方は、ぜひこちらのエンジニア採用サイトをご覧ください。

→ココネ株式会社エンジニアの求人一覧

 

Category

Tag

%d人のブロガーが「いいね」をつけました。