Go/並行処理

< Go
Go/並列処理 から転送)

goroutine編集

goroutine[1] (ゴルーチン)は、Goのランタイムに管理される軽量なスレッドです。

スレッドは並行処理の方式の1つですが、プロセスやタスクと異なり、複数の並行処理単位が同じメモリー空間を共有することで特徴です。OSから見るとメモリー管理の処理が必要なくスレッドの生成・切替を手早くできるという特徴がある一方、他のスレッドとメモリーオブジェクトのアクセス競合が起こらないことの保証はプログラマーの責任になります。

C11(標準C言語の2011年版)にも thread.hstdatmic.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秒経ったら自動的にプログラムを終了する」機能を追加してみましょう。

脚註編集

  1. ^ 言うまでもありませんが、coroutineコルーチンの捩󠄁りです。)

関連項目編集