この記事は エムスリー Advent Calendar 2020 の13日目の記事です。
エムスリーエンジニアリンググループ/BIRの滝安(@juntaki)です。 BIRはビジネスインテリジェンス&リサーチの略で、そこでは医療従事者の会員向けアンケートをベースに、製薬会社へのマーケティング支援を提供する事業を行っています。
BIRはエムスリーの中で比較的Goの開発・運用歴が長いチームです。 チームSlackでも、たまに他チームからのGoよろず相談が行われているのを目にします。
さて、今回の記事では、個人的に作ったCLIツールで発生したGoのスケジューリングの挙動について説明します。 さっそくですが以下のコードはどのような出力になるでしょうか(全然よいコードではないというのはさておき…*1)
※以下の検証はGo 1.15.5で行いました。
func main() { add := make(chan int) sum := 0 go func() { for f := range add { sum += f } }() add <- 1 fmt.Println(sum) }
結果: https://play.golang.org/p/TTrmP3QndKw => 1
加算用のgoroutineを立ち上げて、addに1を入れているので、sumは1になりました。意図したとおりっぽいですね。
それでは、これはどうでしょうか。1ミリ秒のSleepが増えているところ以外は同じです。
func main() { add := make(chan int) sum := 0 go func() { for f := range add { sum += f } }() time.Sleep(1 * time.Millisecond) add <- 1 fmt.Println(sum) }
結果: https://play.golang.org/p/a96xzPkJuk9 => 0
0になります。なにもしてないのに壊れました。
Goのchannelとスケジューラの挙動
この結果の原因はadd <- 1
するタイミングで、addのreceiverがいるかどうか、です。
その差はgoroutineがスイッチするタイミングの違いから発生します。サンプルコードだとスイッチが発生するタイミングは、おおまか下記の2点です。
- channelを操作するとき
- time.Sleepをするとき
最初の例では、add <- 1
するタイミングまで、mainがずっと実行されつづけているので、for f := range add
が動いていません。
なので、mainは、receiverがいないchannelに書き込むためブロックし、Printするまえに加算用のgoroutineにスイッチされます。
2つめの例では、time.Sleep
のタイミングでmainから加算用goroutineにスイッチします。そこでfor f := range add {
が動作します。
なのでadd <- 1
するタイミングでは、receiverがいることになります。
channelに書き込むときは、runtime.chansend()
が呼ばれます。下記はそのコードの抜粋です。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { ... if sg := c.recvq.dequeue(); sg != nil { // Found a waiting receiver. We pass the value we want to send // directly to the receiver, bypassing the channel buffer (if any). send(c, sg, ep, func() { unlock(&c.lock) }, 3) return true } ... gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2) ... }
receiverがいる場合は、c.recvq
からreceiverのgoroutineが取得できるので、メモリコピーでreceiverに値を渡してしまい、そのまま処理を手放すこと無くreturnし次の処理に進みます。一方でreceiverがいない場合は、gopark()
でブロックするコードになっています。
まとめ
コーナーケースをつくようなサンプルコードですが、goroutineの待ち合わせをちゃんと考えて設計しないと、変な挙動に悩まされる、という例でした。
We are hiring!
エムスリー、とくにBIRではGo/Reactエンジニアや、データ基盤開発に興味があるエンジニアを募集しています。社員とカジュアルにお話することもできますので、興味を持たれた方は下記よりお問い合わせください。