Go/並行処理
goroutine
編集goroutine[1] (ゴルーチン)は、Goのランタイムに管理される軽量なスレッドです。
スレッドは並行処理の方式の1つですが、プロセスやタスクと異なり、複数の並行処理単位が同じメモリー空間を共有することで特徴です。OSから見るとメモリー管理の処理が必要なくスレッドの生成・切替を手早くできるという特徴がある一方、他のスレッドとメモリーオブジェクトのアクセス競合が起こらないことの保証はプログラマーの責任になります。
C11(標準C言語の2011年版)にも thread.h と stdatmic.h が用意されたように、近年のコンピューティングではスループットが重視される傾向が強まっています。
- Go文の構文
GoStmt = "go" Expression ;
- 式(
Expression
)は、関数またはメソッドの呼び出しでなければならず、括弧でくくることはできません。組込み関数の呼び出しは式文と同様に制限されます。 - 関数名で示される関数は呼び出される goroutine 側のスレッドですが、引数列の評価は生成元の goroutine で行われます。
- コード例
package main import ( "fmt" "time" ) func main() { go func() { fmt.Println("goroutine .. start") time.Sleep(3 * time.Second) fmt.Println("goroutine .. done") }() report() time.Sleep(5 * time.Second) } func report() { time.Sleep(1 * time.Second) fmt.Println("report .. done") }
- 実行例
report .. done goroutine .. start goroutine .. done
- 解説
go func() { fmt.Println("goroutine .. start") time.Sleep(3 * time.Second) fmt.Println("goroutine .. done") }()
この部分が goroutine で、匿名関数(関数リテラル)の即時実行(即時評価)で実装しています。
time.Sleep(5 * time.Second)
は、goroutine の完了まちです。 main関数が属する goroutine が終われば 他の goroutine が実行中であってもプログラムは終了します。 このため、15行目がないと:
fmt.Println("goroutine .. done")
の前にプログラムが終了します。
チェンネルを使った同期
編集処理を並列化しても、プログラムの終了前に goroutine が完了していることをタイマーで保証していたのでは、プログラム全体の処理時間はタイマー律速になってしまい並列化した意味がありません。
このような問題の解決のために、Go ではチャンネル型が用意されています。
チャネルは、同時に実行される関数が、指定された要素タイプの値を送受信することで通信するためのメカニズムを提供します。
チェンネルを使って同期を取ってみます。
- チェンネルを使った同期
package main import ( "fmt" "time" ) func main() { done := make(chan bool) go func(s int) { fmt.Printf("#%d..do\n", s) time.Sleep(time.Duration(s) * time.Second) fmt.Printf("#%d..done\n", s) done <- true }(2) <-done fmt.Println("Done!") }
- 実行結果
#2..do #2..done Done!
- 解説
done := make(chan bool)
- チェンネル型の変数 done を宣言しています。
- 要素型は bool ですが、今回は同期をとるだけなので要素は何であっても構いません。
- 完了待ち
<-done
- 説明の順番が前後しますが、main 側はチェンネル done の読み込みができたことで goroutine の処理完了をチェックします。
- 値は読んでいませんが、goroutine がチェンネル done に書き込みを行うまで、main はブロックされます。
- 完了通知
done <- true
- coroutine の処理が完了したらチェンネル done に書き込みます。
- これをきっかけに main のブロックは解除され(Done! と表示した後)プログラムを終了します。
これで goroutine の完了後、(無駄な待ち合わせ時間なく)即時にプログラムを完了できます。
Select文を使ったタイムアウト
編集時間内に処理が終わらないというシチュエーションはよくあります。
この様な問題には、Select文を使った問題解決が可能です。
- Select文を使ったタイムアウト
package main import ( "fmt" "time" ) func main() { done := make(chan bool) go func(s int) { fmt.Printf("#%d..do\n", s) time.Sleep(time.Duration(s) * time.Second) fmt.Printf("#%d..done\n", s) done <- true }(2) select { case <-done: fmt.Println("Done!") case <-time.After(1 * time.Second): fmt.Println("Timeout!") } }
- 実行結果
#2..do Timeout!
- 解説
select { case <-done: fmt.Println("Done!") case <-time.After(1 * time.Second): fmt.Println("Timeout!") }
- Select文は、一連の可能な送信または受信操作のうち、どの操作を行うかを選択します。Switch文と似ていますが、ケースがすべて通信操作になっています。
- 19行目のtime.After は指定された時刻を経過するとチャネルからメッセージが送られてきます。
このように Go では通信操作の枠組みで同期やタイムアウトの仕組みを統合しています。
Ticker と Signal
編集time.After は指定された時刻を経過するとチャネルからメッセージが送られてきますが、time.NewTicker は与えられた間隔で time オブジェクトを返します。
signal パッケージは、OSシグナルをチャンネルにルーティングします。
- Ticker と Signal
package main import ( "fmt" "os" "os/signal" "syscall" "time" ) func main() { ticker := time.NewTicker(time.Second) defer ticker.Stop() sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, ) defer signal.Stop(sig) for { select { case now := <-ticker.C: fmt.Println(now) case s := <-sig: switch s { case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT: fmt.Println(s) fmt.Println("Done") return } } } }
- 実行結果(3秒経過したところで [CTRL] + C で中断した)
2021-09-21 11:04:43.522706401 +0900 JST m=+1.031904806 2021-09-21 11:04:44.522783142 +0900 JST m=+2.031989090 2021-09-21 11:04:45.522671031 +0900 JST m=+3.031881449 ^Cinterrupt Done
- 解説
ticker := time.NewTicker(time.Second) defer ticker.Stop()
- ticker オブジェクトを生成します
time.Second
は、1 * time.Second
と同義です。 - defer 遅延処理で、この場合
ticker.Stop()
はmain関数が return したときに実行されます。 - (開いたファイルを自動で閉じるなど)C++のデストラクターの様な振る舞いが求められるときに便利です。
- シグナルの前処理
sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, ) defer signal.Stop(sig)
- Go のシグナル通知は、チャネルに os.Signal 値を送信することで行います。
- signal.Notify で、指定されたシグナル通知を受信するために、チャネル sig を登録します。
- 着信シグナルの中継を停止する処理を、defer で登録します。
- TickerとSignalの並列待ち
select { case now := <-ticker.C: fmt.Println(now) case s := <-sig: switch s { case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT: fmt.Println(s) fmt.Println("Done") return } }
- Select文でインターバル発火とシグナルを並列に待っています。
- チャンネル ticker.C からは与えられたインターバルで時刻が送信されます。
- チャンネル sig からがOSのシグナルが中継されます。
- このSelect文の外周の for{ の無限ループは
return
- が唯一の脱出点なので、シグナルが来ないと止まりません。
- 課題
- 上のプログラムを編集して、「15秒経ったら自動的にプログラムを終了する」機能を追加してみましょう。