
Go並列実行時のmapについて
-
2023年7月18日
お疲れ様です。サーバーエンジニアの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を使用しましたが、他にも対応方法はあるので実装に応じて検討してみて下さい。