Go言語における並行処理の注意点

~ sync.WaitGroupとmap編 ~

Ryosuke Sugihara
Eureka Engineering
6 min readNov 30, 2018

--

はじめに

エウレカで今年新卒入社した Ryosuke Sugiharaです。現在はAPI Teamに所属しています。好きな食べ物は🍣と🍜、趣味はTwitterとサバゲーです。

最近API Teamではモブプロをやるようになり、一人で悩んでいたところをすぐ議論できその場で解決していけるので大きめなタスクをやる際に適しているなと感じています。

さて今回は社内で定期的に開催されるGo言語勉強会でsync.WaitGroupとMapの話がでて面白かったので記事にしたいと思います。

sync.WaitGroupとgoroutineについて

Go言語には並行処理をうまく行う仕組みがあり、そのうちの一つがsync.WaitGroupです。ここではsync.WaitGroupの使い方とよくある問題について書きます。

sync.WaitGroupは複数のgoroutineの完了を待つために使用されます。

package main

import (
"fmt"
"sync"
"time"
)

func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func (i int) {
defer wg.Done()
fmt.Println(i)
time.Sleep(1)
}(i)
}
wg.Wait()
fmt.Println("complete!")
}

sync.WaitGroupを作成し、wg.Add(1)で値を追加し、Wg.Done()で終了を知らせます。

wg.Wait()で終了を待つことができ、wg.Wait()がなければgoroutineが動くよりも前にmain関数が終了してしまいます。 参考

また、以下のコードはやりがちなのですが問題があります。

package main

import (
"fmt"
"sync"
"time"
)

func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func () {
defer wg.Done()
fmt.Println(i)
time.Sleep(1)
}()
}
wg.Wait()
fmt.Println("complete!")
}

実行すると0~19が一回ずつ出力されるようになっていません。これはgoroutineが実行される前にiの値が変わってしまっているためです。 参考

mapと競合

Go言語ではハッシュ(連想配列)のことをmapと呼びます。ここでは並行処理でmapを扱った時に発生する問題に関して書きます。

以下のコードを見てみましょう。goroutineでmapにキーと値を入れています。

package main

import (
"fmt"
"sync"
"time"
)

func main() {
m := make(map[interface{}]interface{})
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func (i int) {
defer wg.Done()
m[i] = i
time.Sleep(1)
}(i)
}
wg.Wait()
fmt.Println(m)
}

実行すると問題なく処理が終わりますが、go run -race (任意の名前).go で実行をすると以下のようなWarningがでます。

==================
WARNING: DATA RACE
Write at 0x00c000090180 by goroutine 7:
runtime.mapassign()
/Users/godslew/.goenv/versions/1.11.2/src/runtime/map.go:549 +0x0
main.main.func1()
/Users/godslew/scratch_3.go:16 +0xd8
Previous write at 0x00c000090180 by goroutine 6:
runtime.mapassign()
/Users/godslew/.goenv/versions/1.11.2/src/runtime/map.go:549 +0x0
main.main.func1()
/Users/godslew/scratch_3.go:16 +0xd8
Goroutine 7 (running) created at:
main.main()
/Users/godslew/scratch_3.go:14 +0xd3
Goroutine 6 (running) created at:
main.main()
/Users/godslew/scratch_3.go:14 +0xd3
==================
map[1:1 3:3 18:18 17:17 19:19 2:2 7:7 9:9 11:11 12:12 14:14 0:0 4:4 5:5 8:8 16:16 6:6 10:10 13:13 15:15]
Found 1 data race(s)
exit status 66

ループの回数が増えるとGoの方でも検知してくれます。 参考

以上のことからに並行処理を行う上で競合をしないように書く必要があります。

package mainimport (
"fmt"
"sync"
"time"
)
func main() {
mutex := &sync.Mutex{}
m := make(map[interface{}]interface{})
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
mutex.Lock()
m[i] = i
mutex.Unlock()
time.Sleep(1)
}(i)
}
wg.Wait()
fmt.Println(m)
}

新しく sync.Mutex を使い排他制御しています。この状態でもう一度 go run -race (任意の名前).go 実行するとWarningが出なくなっていることが確認できます。

まとめ

sync.WaitGroupを使うと複数のgoroutineの終了を待つことができ、それら全てのgoroutineの処理が完了してから次の処理をしたいときに恩恵が受けられます。また、並行処理において一回はハマる問題についても触れました。

並行処理でmapに書き込むと競合が起きる問題については勉強会で初めて知り、sync.Mutexがどのようなものか理解するきっかけにもなりました。

--

--

Ryosuke Sugihara
Eureka Engineering

eureka, Inc API team Pairsのweb版開発を担当しています。 主にバックエンドを担当、フロント少々。 Twitterが好きで学生時代はTwitterアプリの開発ばかりやっていた。