2022/06/12

[golang] chan で待つ (2)

golang の goroutine は、確か実装依存だったと思う。
Linux だったらきっと pthread なんだろうな、と思っていたが、そうではなくて自前で並列処理を作っていたような記憶がある。
まあ、使う方としてはそこまで意識をしないけど、OS 無しだったり独自OS だとどうなるのか気になるね。


さて、前回は 1つのチャネルで 1つの処理が終わるのを待つのを確認した。処理結果がいらない場合にわざわざ struct{}{} を返すので、別に bool とかでいいんじゃないとも思ったが、本に struct{}{} を返すよう書かれているので、そういうお作法と考えるのが良いだろう。値を返す、ということ自体が結構めんどうな処理になりそうだしね。

では複数の goroutine が全部終わるのを待つ場合も考えよう。
make() を使うので第2引数に待ちたいだけの数を設定して待てばよさそうな気がしたのだが、for で待つにしてもどの順番で処理が終わるか分からないから、make() で 3つ作って順に 処理A, B, C に割り当てたとすると、A を待っている間に B, C が先に終わってしまうこともあるだろう。その後で A が終わって待ち解除になると B 待ち状態になるが、既に処理が終わった B についてどうなるのかわからん。


 それはともかく、make()で複数の chan を作るのは「バッファ有りチャネル」として紹介されていた。
本の説明では 3つ作っている。そして 4つめは「送信文は待たされます」と書かれている。ということは chan は受信待ちだけでなく送信待ちというものもあるということか。
ただ、これは対象がスライス全体を与えているからそうなるのだと思う。スライスの各要素で送受信するようにしたらバッファ無しのチャネルと同じになるはずだ。

あるいは、スライス全体にすることで「早い者勝ち」になるようだから、それをループで回数だけ待てばよいはずだ。

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Printf("start\n")
	done := make(chan struct{}, 3)
	go func() {
		fmt.Printf("goroutine start - 1\n")
		time.Sleep(10 * time.Second)
		fmt.Printf("goroutine done - 1\n")
		done <- struct{}{}
	}()
	go func() {
		fmt.Printf("goroutine start - 2\n")
		time.Sleep(5 * time.Second)
		fmt.Printf("goroutine done - 2\n")
		done <- struct{}{}
	}()
	go func() {
		fmt.Printf("goroutine start - 3\n")
		time.Sleep(3 * time.Second)
		fmt.Printf("goroutine done - 3\n")
		done <- struct{}{}
	}()
	for range done {
		<-done
	}
	fmt.Printf("done\n")
}

実行。

$ go run .
start
goroutine start - 3
goroutine start - 1
goroutine start - 2
goroutine done - 3
goroutine done - 2
goroutine done - 1
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        /home/xxx/golang/src/chan/main.go:30 +0x125
exit status 2

あれ、ダメなんだ。
for の回数を固定させるとエラーは起きないようなので range done

package main

import (
	"fmt"
	"time"
)

func main() {
	const eventNum = 3
	fmt.Printf("start\n")
	done := make(chan struct{}, eventNum)
	go func() {
		fmt.Printf("goroutine start - 1\n")
		time.Sleep(10 * time.Second)
		fmt.Printf("goroutine done - 1\n")
		done <- struct{}{}
	}()
	go func() {
		fmt.Printf("goroutine start - 2\n")
		time.Sleep(5 * time.Second)
		fmt.Printf("goroutine done - 2\n")
		done <- struct{}{}
	}()
	go func() {
		fmt.Printf("goroutine start - 3\n")
		time.Sleep(3 * time.Second)
		fmt.Printf("goroutine done - 3\n")
		done <- struct{}{}
	}()
	for i := 0; i < eventNum; i++ {
		<-done
	}
	fmt.Printf("done\n")
}

実行。

$ go run .
start
goroutine start - 3
goroutine start - 1
goroutine start - 2
goroutine done - 3
goroutine done - 2
goroutine done - 1
done

本には、

  • len() は現在バッファされている要素の個数を返す
  • cap() はバッファ容量を返す

とある。
range donelen(done) と同じルールならダメそうだ。
本によると、 range でチャネルを回すのはチャネルに対して送信されたすべての値を受信するときと書かれているので len()と同じなのだろう。

いろいろやってみらんとわからんもんやね。


今日は StackEdit で Blogger にアップしているのだけど、画像の貼り付け方がわからん。
Google Photo の共有リンクはダメだった。Open Live Writer だとそこはうまいことやってくれるのだけど、あれは自前でビルドしないと最新版は Microsoft Store に上がってないから Blogger では使えないのだ。