Chapter 03

ゴールーチンとチャネル

さき(H.Saki)
さき(H.Saki)
2021.06.18に更新

この章について

Goで並行処理を扱う場合、主に以下の道具が必要になります。

  • ゴールーチン
  • sync.WaitGroup
  • チャネル

これらについて説明します。

ゴールーチン

定義

ゴールーチンの定義は、Goの言語仕様書で触れられています。

A "go" statement starts the execution of a function call as an independent concurrent thread of control, or goroutine, within the same address space.
(訳) go文は渡された関数を、同じアドレス空間中で独立した並行スレッド(ゴールーチン)の中で実行します。

出典:The Go Programming Language Specification#Go_statements

噛み砕くと、ゴールーチンとは「他のコードに対し並行に実行している関数」です。

ゴールーチン作成

実際にgo文を使ってゴールーチンを作ってみましょう。

まずは「今日のラッキーナンバーを占って表示する」getLuckyNum関数を用意しました。

func getLuckyNum() {
	fmt.Println("...")

	// 占いにかかる時間はランダム
	rand.Seed(time.Now().Unix())
	time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond)

	num := rand.Intn(10)
	fmt.Printf("Today's your lucky number is %d!\n", num)
}

これを新しく作ったゴールーチンの中で実行してみましょう。

func main() {
	fmt.Println("what is today's lucky number?")
	go getLuckyNum()

	time.Sleep(time.Second * 5)
}
(実行結果)
what is today's lucky number?
...
Today's your lucky number is 1!

このとき、実行の様子の一例としては以下のようになっています。

ゴールーチンの待ち合わせ

待ち合わせなし

ここで、メインゴールーチンの中に書かれていた謎のtime.Sleep()を削除してみましょう。

func main() {
	fmt.Println("what is today's lucky number?")
	go getLuckyNum()

-	time.Sleep(time.Second * 5)
}
(実行結果)
what is today's lucky number?

ラッキーナンバーの結果が出る前にプログラムが終わってしまいました。
これはGoが「メインゴールーチンが終わったら、他のゴールーチンの終了を待たずにプログラム全体が終わる[1]」という挙動をするからです。

待ち合わせあり

メインゴールーチンの中で、別のゴールーチンが終わるのを待っていたい場合はsync.WaitGroup構造体の機能を使います。

func main() {
	fmt.Println("what is today's lucky number?")
-	go getLuckyNum()
-
-	time.Sleep(time.Second * 5)

+	var wg sync.WaitGroup
+	wg.Add(1)
+
+	go func() {
+		defer wg.Done()
+		getLuckyNum()
+	}()
+
+	wg.Wait()
}

sync.WaitGroup構造体は、内部にカウンタを持っており、初期化時点でカウンタの値は0です。

ここでは以下のように設定しています。

  1. sync.WaitGroup構造体wgを用意する
  2. wg.Add(1)で、wgの内部カウンタの値を+1する
  3. defer wg.Done()で、ゴールーチンが終了したときにwgの内部カウンタの値を-1するように設定
  4. wg.Wait()で、wgの内部カウンタが0になるまでメインゴールーチンをブロックして待つ

sync.WaitGroupを使って書き換えたコードを実行してみましょう。

(実行結果)
what is today's lucky number?
...
Today's your lucky number is 7!

今日のラッキーナンバーが表示されて、ちゃんと「サブのゴールーチンが終わるまでメインを待たせる」という期待通りの挙動を得ることができました。
いわゆる「同期をとる」という作業をここで実現させています。

チャネル

定義

チャネルとは何か?というのは、言語仕様書のチャネル型の説明ではこのように定義されています。

A channel provides a mechanism for concurrently executing functions to communicate by sending and receiving values of a specified element type.

(訳) チャネルは、特定の型の値を送信・受信することで(異なるゴールーチンで)並行に実行している関数がやり取りする機構を提供しています。

出典:The Go Programming Language Specification#Channel_types

また、GoCon 2021 Springで行われたMofizur Rahman(@moficodes)さんによるチャネルについてのセッションでは以下のように述べられました。

Channels are a typed conduit through which you can send and receive values with the channel operator, <-.

(訳) チャネルは、チャネル演算子<-を使うことで値を送受信することができる型付きの導管です。

動画:Go Conference 2021 Spring Track A (該当箇所1:02:44)
スライド:Go Channels Demystified

どちらの定義でも共有して述べられているのは、チャネルは「異なるゴールーチン同士が、特定の型の値を送受信することでやりとりする機構」であるということです。

言葉だけだとわかりにくいでしょうから、先ほどのラッキーナンバーの実例を使って説明していきます。

チャネルを使った値の送受信

仕様変更

今までは「標準出力にラッキーナンバーを表示する」機構は、getLuckyNumの方にありました。

func getLuckyNum() {
	// (略)
	fmt.Printf("Today's your lucky number is %d!\n", num)
}

これを、メインゴールーチンの方で行うように仕様変更することを考えます。

func getLuckyNum() {
	// (略)
- 	fmt.Printf("Today's your lucky number is %d!\n", num)
+	// メインゴールーチンにラッキーナンバーnumをどうにかして伝える
}

func main() {
	fmt.Println("what is today's lucky number?")
	go getLuckyNum()

+	// ゴールーチンで起動したgetLuckyNum関数から
+	// ラッキーナンバーを変数numに取得してくる

+	fmt.Printf("Today's your lucky number is %d!\n", num)
}

この仕様変更によって

  • getLuckyNum関数を実行しているゴールーチンからメインゴールーチンに値を送信する
  • メインゴールーチンがgetLuckyNum関数を実行しているゴールーチンから値を受信する

という2つの機構が必要になりました。
これを実装するのに、「異なるゴールーチン同士のやり取り」を補助するチャネルはぴったりの要素です。

実装

実際にチャネルを使って実装した結果は以下の通りです。

func getLuckyNum(c chan<- int) {
	fmt.Println("...")

	// ランダム占い時間
	rand.Seed(time.Now().Unix())
	time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond)

	num := rand.Intn(10)
	c <- num
}

func main() {
	fmt.Println("what is today's lucky number?")

	c := make(chan int)
	go getLuckyNum(c)

	num := <-c

	fmt.Printf("Today's your lucky number is %d!\n", num)

	// 使い終わったチャネルはcloseする
	close(c)
}

やっていることとしては

  1. make(chan int)でチャネルを作成 → getLuckyNum関数に引数として渡す
  2. getLuckyNum関数内で得たラッキーナンバーを、チャネルcに送信(c <- num)
  3. メインゴールーチンで、チャネルcからラッキーナンバーを受信(num := <-c)

です。

これを実行してみると、以下のように期待通りの挙動をすることが確認できます。

(実行結果)
what is today's lucky number?
...
Today's your lucky number is 3!
脚注
  1. 参考までにOSのプロセスの場合、親プロセスが終了したときにまだ残っていた子プロセスは強制終了されることなく「孤児プロセス」と呼ばれ、代わりにinitプロセスを親にする紐付けが行われます。 ↩︎