はじめに
Goのgoroutineついてわかりやすく説明することを目標に記事を書きました。
要点
・goの並行処理goroutineの実装は至って簡単で、並行処理させたい関数の前に go をつけるだけ。
・並行処理はメインスレッドとは異なるスレッドで同時に実行する。メインスレッドが終了したら実行中のgoroutineも強制終了
・goroutineを必ず実行させたいなら、WaitGroupなどの機能を利用しよう。
実際にコードを見てみよう
package main
import (
"fmt"
"time"
)
func rush(shout string) {
for i := 0; i < 4; i++ {
fmt.Println(shout)
time.Sleep(time.Second)
}
}
func main() {
go rush("無駄")
rush("オラ")
}
これで上のコードを実行すると、下の結果になります。
オラ
無駄
無駄
オラ
オラ
無駄
無駄
オラ
ご覧の通り、go rush("無駄") の部分が並行実行され、文字が不規則に交互に出力されます。ここで func rush 内のtime.Sleep(time.Second) を削除してみましょう。すると下の出力になります。
オラ
オラ
オラ
オラ
あれ?何回やっても"無駄"が出力されませんね。
実はGoはメインスレッドというメインの処理が流れる場所があり、go routineはそのメインスレッドとは別にサブのスレッドを用意し、そこで処理を並行実行するのです。
ここで重要なことは、goはメインスレッドの処理が終了したら他のサブスレッドの状況に関係なく動作を終了してしまうのです。
そのため、 time.Sleep(time.Second) を削除するとメインスレッドが一切の待機時間なく処理を行い、 rush("オラ") がループの最大値まで即座に実行されます。するとサブスレッドで実行中の go rush("無駄") は割り込む間も見つけれず、何もできずに終了してしまうのです。
また、試しに両方ともgoroutineにしてみましょう。
func main() {
go rush("無駄")
go rush("オラ")
}
今度は何も出力されません。これは両方の処理がそれぞれ異なるサブスレッドに移動し、メインスレッドでは何もしないため、即座にメインスレッドが終了してしまったのです。よって両者共に強制終了となりました。
ではどうすればメインスレッドが終わってもgoroutineが終わらないようにできるのでしょうか。そこで sync パッケージの WaitGroupを使用してみましょう。
package main
import (
"fmt"
"sync"
"time"
)
func mainRush(shout string) {
for i := 0; i < 4; i++ {
fmt.Println(shout)
}
}
func subRush(wg *sync.WaitGroup, shout string) {
defer wg.Done()
for i := 0; i < 4; i++ {
fmt.Println(shout)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
go subRush(&wg, "無駄")
mainRush("オラ")
wg.Wait()
}
実行結果は下のようになります。
オラ
オラ
オラ
オラ
無駄 // メインスレッドが終了しても実行してくれる
無駄
無駄
無駄
WaitGroupの Add() メソッドはカウンターを+1(初期値0)し、 Done() メソッドはカウンターを-1します。そして、 Wait() メソッドはWaitGroupのカウンターが 0 になるまで待ちます。これら機能を利用することでメインスレッドが終了してもgoroutineを実行することができます。
注意しなければならないのはカウンターを減算する処理を忘れてしまうといつまで経っても処理が終了しないdead lock状態になってしまいます。試しに func subRush() の defer wg.Done() を消してみてください。dead lockが発生します。