goroutineがスイッチされるタイミングについて調べていました。
結論
Go言語で、goroutineは 必ずしも スイッチされるわけではない。
スタックに触れないような、「for(){}」みたいなビジーループをGOMAXPROCSの指定数以上に含ませるとスイッチされなくなる。
goroutineがスイッチされる(主な)条件はこれらと思われる。
- goroutineの関数が最適化でinline化されていない
- スタックを操作するような処理を行った
- (その他の契機もあるようなので「経緯」で書く)
経緯
処理のないビジーループが有ると、goroutineがスイッチされず処理が止まることに気づきました。
# 処理の中身を全部コメントアウトしてデバッグしていたら気づいた
package main
func busy() {
for {
}
}
func main() {
go busy()
go func() {
println("hello")
}()
i := make(chan int)
<-i
}
お行儀悪いこと甚だしいコードですが、並列処理なのにスイッチされない理由は気になります。
そういうわけで調べていると、参考に上げたStackOverflowが当たりました。これによると、
- Goのgoroutineのスケジューラはノンプリエンプティブ(協調型マルチタスク)である
- 主に、以下の契機でスケジューラに制御がわたり、スイッチが起こされる
- アンバッファなチャネルへの読み書き
- システムコール呼び出し
- メモリアロケーション
- time.Sleep()が呼ばれる
- runtime.Gosched()が呼ばれる
との事でした。
そういうわけで一見落着したかと思いましたが、busy()を以下の用に書き換えて追試した所、
func busy() {
for i:=0; i<1e8; i++ {
}
}
hello
fatal error: all goroutines are asleep - deadlock!
上手く動いてしまい、コードの期待通りデッドロック警報がでてしまいました。
いやでもこれはおかしい。先と同じビジーループであって、スケジューラに拾わせに行くタイミングが存在しないからです。
コレは誤認でした、コメント参照
不思議に思っていたんですが、よく見るとstackoverflowの解答時期がちょっと古かったので、念の為Go 1.2のリリースノートに当たってみました。
Pre-emption in the scheduler
In prior releases, a goroutine that was looping forever could starve out other goroutines on the same thread, a serious problem when GOMAXPROCS provided only one user thread. In Go 1.2, this is partially addressed: The scheduler is invoked occasionally upon entry to a function. This means that any loop that includes a (non-inlined) function call can be pre-empted, allowing other goroutines to run on the same thread.
ビンゴ!
インライン関数になっていなければ、スイッチできるようになったらしいです。
# インライン関数はgcが最適化時に勝手に作る
では、本当にbusy()インライン関数化されたためにスイッチされなかったのか、gcに"-m"オプションをつけて最適化状態を確認していきます。
umisama-X1c{umisama}% go build -gcflags -m test1.go
# command-line-arguments
./test.go:11: can inline func·001
./test.go:11: func literal escapes to heap
./test.go:15: main make(chan int, 0) does not escape
11行目はprintしている関数なので、busy()はインライン化されていませんでした。
アタリが外れてしまいました。
そういうわけで、Go 1.2で入ったスケジューラ周りの変更を読みます。恐らく、リリースノートにあったスイッチに関する変更はこのコミットだと思われます。
https://code.google.com/p/go/source/detail?r=575afd15c877
diffを読むと、当該の変更は主にスタック操作時に入ったことがわかります。
「for(){}」と書くと純粋なJUMPになり、スタックを触りません。したがって、スイッチされる契機を得ることは出来なさそうです。
逆に、ループ回数を計算する場合は変数を触る->スタックを触ったため契機を得た模様。
自信は無いけど多分そんな感じ。
とりあえずスッキリした。
参考
http://stackoverflow.com/questions/17953269/go-routine-blocking-the-others-one
http://golang.org/doc/go1.2
https://code.google.com/p/go/issues/detail?id=543