Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 1549] ファイルの概要

このコミットは、Go言語のテストスイート内の test/chan/nonblock.go ファイルに対する変更です。具体的には、チャネルの非ブロッキング動作をテストする際に、Goランタイムが実際のOSスレッドを使用する環境下でもテストが正しく機能するように修正が加えられています。

コミット

commit d2117ad43874c6729ee532da1f73ddfa7ab2ed46
Author: Russ Cox <rsc@golang.org>
Date:   Fri Jan 23 17:04:56 2009 -0800

    make test/chan/nonblock work even with real os threads
    
    R=ken
    OCL=23422
    CL=23422

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d2117ad43874c6729ee532da1f73ddfa7ab2ed46

元コミット内容

make test/chan/nonblock work even with real os threads

このコミットメッセージは、test/chan/nonblock テストが「実際のOSスレッド」を使用する環境でも動作するように修正されたことを示しています。これは、Goランタイムのスケジューリングモデルが進化し、より直接的にOSスレッドを利用するようになったことへの対応と考えられます。

変更の背景

Go言語の初期のバージョンでは、ゴルーチン(Goの軽量スレッド)のスケジューリングは、Goランタイムが管理するM:Nスケジューリングモデル(M個のゴルーチンをN個のOSスレッドにマッピングする)に基づいていました。このモデルでは、ゴルーチンは協調的に(sys.Gosched()のような関数を呼び出すことで)CPUを譲り渡すことが期待される場合がありました。

しかし、実際のOSスレッドが関与するようになると、協調的なスケジューリングだけでは不十分になる場合があります。特に、チャネルの非ブロッキング操作のような時間依存のテストでは、ゴルーチンの実行順序やタイミングがOSスレッドのスケジューリングに影響される可能性が出てきます。

このコミットが行われた2009年1月は、Go言語がまだ公開される前の非常に初期の段階です。この時期は、Goランタイムのスケジューラや並行処理モデルが活発に開発・改善されていた時期にあたります。test/chan/nonblock.go は、チャネルの非ブロッキング送受信が期待通りに動作するかを検証するためのテストであり、Goの並行処理の根幹に関わる重要なテストです。

「real os threads」という記述は、GoランタイムがOSスレッドをより積極的に利用するようになった、あるいはその利用方法が変更されたことを示唆しています。これにより、テストのタイミングが変わり、従来の pause() 関数(内部で sys.Gosched() を複数回呼び出す)だけでは、テストが不安定になったり、失敗するようになった可能性があります。この変更は、Goランタイムの進化に合わせて、テストの堅牢性を高めることを目的としています。

前提知識の解説

Goのゴルーチンとチャネル

  • ゴルーチン (Goroutines): Go言語における軽量な実行スレッドです。OSスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行できます。Goランタイムがゴルーチンのスケジューリング、スタックの管理、OSスレッドへのマッピングを行います。
  • チャネル (Channels): ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは、Goの並行処理モデルであるCSP (Communicating Sequential Processes) の中心的な要素です。チャネルにはバッファリングされたものとされていないものがあり、それぞれ異なるブロッキング特性を持ちます。
    • バッファなしチャネル: 送信側と受信側が同時に準備ができていないと、どちらかの操作がブロックされます。
    • バッファありチャネル: バッファが満杯でない限り送信はブロックされず、バッファが空でない限り受信はブロックされません。

GoスケジューラとOSスレッド

Goランタイムには、ゴルーチンをOSスレッドにマッピングし、実行を管理するスケジューラが組み込まれています。初期のGoスケジューラは、ゴルーチンが明示的に sys.Gosched() を呼び出すことで、他のゴルーチンにCPUを譲る「協調的スケジューリング」の側面を持っていました。しかし、現代のGoスケジューラは、よりプリエンプティブ(横取り型)なスケジューリングに進化しており、ゴルーチンが長時間CPUを占有することを防ぎ、より公平な実行を保証しています。

sys.Gosched()

sys.Gosched() は、現在のゴルーチンがCPUを他のゴルーチンに譲ることをスケジューラに示唆する関数です。これは協調的マルチタスクの典型的な例であり、ゴルーチンが自発的に実行を中断し、他のゴルーチンに実行機会を与えるために使用されました。しかし、この方法は、ゴルーチンが Gosched を呼び出さないと他のゴルーチンが実行されない可能性があるため、デッドロックやパフォーマンスの問題を引き起こす可能性がありました。

time.Tick

time.Tick は、指定された期間ごとにイベントを発生させるチャネルを返します。これは、定期的な処理やタイムアウトの実装によく使用されます。このコミットでは、time.Tick を使用して、より信頼性の高い時間ベースの待機メカニズムを導入し、sys.Gosched() のみによる協調的な待機を置き換えています。

技術的詳細

このコミットの主要な変更点は、test/chan/nonblock.go テストにおけるゴルーチンの同期と待機メカニズムの改善です。

  1. pause() 関数の削除:

    • 元のコードでは、pause() 関数が for i:=0; i<100; i++ { sys.Gosched() } というループで sys.Gosched() を100回呼び出すことで、他のゴルーチンに実行機会を与えようとしていました。
    • この方法は、Goランタイムが「実際のOSスレッド」を使用するようになった環境では、その効果が不安定になる可能性がありました。OSスレッドのスケジューリングはGoランタイムの制御外であり、sys.Gosched() を呼び出しても、必ずしも期待通りに他のゴルーチンがすぐに実行されるとは限りません。
  2. import "time" の追加と ticker の導入:

    • import "time" が追加され、var ticker = time.Tick(10*1000); // 10 us が定義されました。これは、10マイクロ秒ごとに値を送信するチャネルを作成します。
  3. sleep() 関数の導入:

    • 新しい sleep() 関数は、<-ticker; <-ticker; sys.Gosched(); sys.Gosched(); sys.Gosched(); と実装されています。
    • <-ticker; を2回呼び出すことで、少なくとも20マイクロ秒の時間が経過するのを待ちます。これにより、OSスケジューラが他のゴルーチンに実行を切り替えるための十分な時間的余裕が生まれます。
    • その後の sys.Gosched() の呼び出しは、念のための協調的スケジューリングのヒントとして残されていますが、time.Tick による時間経過の保証が主要な待機メカニズムとなっています。
  4. strobe チャネルによるゴルーチン間の明示的な同期:

    • i32receiver, i32sender, i64receiver, i64sender, breceiver, bsender, sreceiver, ssender といったチャネル操作を行うヘルパー関数に、新たに strobe chan bool という引数が追加されました。
    • これらのヘルパー関数は、チャネル操作が完了した後に strobe <- true を実行し、完了を通知するようになりました。
    • main 関数では、var sync = make(chan bool) というバッファなしチャネルが導入され、この sync チャネルが strobe として各ヘルパー関数に渡されます。
    • これにより、main 関数は <-sync を使用して、ヘルパーゴルーチンがチャネル操作を完了したことを明示的に待つことができるようになりました。これは、テストの実行順序とタイミングをより厳密に制御するために非常に重要です。
  5. テストロジックの変更:

    • main 関数内のテストループでは、go i32receiver(c32, sync); sleep(); のように、ゴルーチンを起動した後に sleep() を呼び出し、その後 ok = c32 <- 123; のようにチャネル送信を行います。
    • そして、<-sync; を呼び出すことで、受信側ゴルーチンが値を受け取ったことを確認します。
    • 送信側ゴルーチンについても同様に、go i32sender(c32, sync); の後に if buffer > 0 { <-sync } else { sleep() } のような条件付きの同期が追加されています。バッファ付きチャネルの場合は送信がブロックされないためすぐに <-sync で完了を待ち、バッファなしチャネルの場合は sleep() で少し待ってから受信側が値を受け取るのを待ちます。
    • これらの変更により、テストはゴルーチンの実行とチャネル操作の完了をより確実に同期できるようになり、OSスレッドのスケジューリングの変動に左右されにくくなりました。

コアとなるコードの変更箇所

--- a/test/chan/nonblock.go
+++ b/test/chan/nonblock.go
@@ -9,40 +9,55 @@
 
 package main
 
-func pause() {
-	for i:=0; i<100; i++ { sys.Gosched() }
-}
+import "time"
 
-func i32receiver(c chan int32) {
+func i32receiver(c chan int32, strobe chan bool) {
 	if <-c != 123 { panic("i32 value") }
+	strobe <- true
 }
 
-func i32sender(c chan int32) {
-	c <- 234
+func i32sender(c chan int32, strobe chan bool) {
+	c <- 234;
+	strobe <- true
 }
 
-func i64receiver(c chan int64) {
+func i64receiver(c chan int64, strobe chan bool) {
 	if <-c != 123456 { panic("i64 value") }
+	strobe <- true
 }
 
-func i64sender(c chan int64) {
-	c <- 234567
+func i64sender(c chan int64, strobe chan bool) {
+	c <- 234567;
+	strobe <- true
 }
 
-func breceiver(c chan bool) {
+func breceiver(c chan bool, strobe chan bool) {
 	if ! <-c { panic("b value") }
+	strobe <- true
 }
 
-func bsender(c chan bool) {
-	c <- true
+func bsender(c chan bool, strobe chan bool) {
+	c <- true;
+	strobe <- true
 }
 
-func sreceiver(c chan string) {
+func sreceiver(c chan string, strobe chan bool) {
 	if <-c != "hello" { panic("s value") }
+	strobe <- true
+}
+
+func ssender(c chan string, strobe chan bool) {
+	c <- "hello again";
+	strobe <- true
 }
 
-func ssender(c chan string) {
-	c <- "hello again"
+var ticker = time.Tick(10*1000);	// 10 us
+func sleep() {
+	<-ticker;
+	<-ticker;
+	sys.Gosched();
+	sys.Gosched();
+	sys.Gosched();
 }
 
  func main() {
@@ -52,45 +67,57 @@ func main() {
  	var s string;
  	var ok bool;
 
+	var sync = make(chan bool);
+
  	for buffer := 0; buffer < 2; buffer++ {
  	\tc32 := make(chan int32, buffer);
  	\tc64 := make(chan int64, buffer);
@@ -70,45 +97,57 @@ func main() {
  	\ts, ok = <-cs;
  	\tif ok { panic("blocked ssender") }
 
-\t\tgo i32receiver(c32);
-\t\tpause();
+\t\tgo i32receiver(c32, sync);
+\t\tsleep();
  	\tok = c32 <- 123;
-\t\tif !ok { panic("i32receiver") }\n-\t\tgo i32sender(c32);\n-\t\tpause();
+\t\tif !ok { panic("i32receiver buffer=", buffer) }
+\t\t<-sync;
+\n+\t\tgo i32sender(c32, sync);
+\t\tif buffer > 0 { <-sync } else { sleep() }
  	\ti32, ok = <-c32;
-\t\tif !ok { panic("i32sender") }
+\t\tif !ok { panic("i32sender buffer=", buffer) }
  	\tif i32 != 234 { panic("i32sender value") }
+\t\tif buffer == 0 { <-sync }
 
-\t\tgo i64receiver(c64);
-\t\tpause();
+\t\tgo i64receiver(c64, sync);
+\t\tsleep();
  	\tok = c64 <- 123456;
  	\tif !ok { panic("i64receiver") }
-\t\tgo i64sender(c64);\n-\t\tpause();
+\t\t<-sync;
+\n+\t\tgo i64sender(c64, sync);
+\t\tif buffer > 0 { <-sync } else { sleep() }
  	\ti64, ok = <-c64;
  	\tif !ok { panic("i64sender") }
  	\tif i64 != 234567 { panic("i64sender value") }
+\t\tif buffer == 0 { <-sync }
 
-\t\tgo breceiver(cb);
-\t\tpause();
+\t\tgo breceiver(cb, sync);
+\t\tsleep();
  	\tok = cb <- true;
  	\tif !ok { panic("breceiver") }
-\t\tgo bsender(cb);\n-\t\tpause();
+\t\t<-sync;
+\n+\t\tgo bsender(cb, sync);
+\t\tif buffer > 0 { <-sync } else { sleep() }
  	\tb, ok = <-cb;
  	\tif !ok { panic("bsender") }
  	\tif !b{ panic("bsender value") }
+\t\tif buffer == 0 { <-sync }
 
-\t\tgo sreceiver(cs);
-\t\tpause();
+\t\tgo sreceiver(cs, sync);
+\t\tsleep();
  	\tok = cs <- "hello";
  	\tif !ok { panic("sreceiver") }
-\t\tgo ssender(cs);\n-\t\tpause();
+\t\t<-sync;
+\n+\t\tgo ssender(cs, sync);
+\t\tif buffer > 0 { <-sync } else { sleep() }
  	\ts, ok = <-cs;
  	\tif !ok { panic("ssender") }
  	\tif s != "hello again" { panic("ssender value") }
+\t\tif buffer == 0 { <-sync }
  	}\n  	print("PASS\\n")
  }

コアとなるコードの解説

このコミットの核となる変更は、test/chan/nonblock.go テストの同期メカニズムを、協調的な sys.Gosched() ベースの pause() から、time.Tick と明示的なチャネル同期 (strobe/sync チャネル) を組み合わせたより堅牢な方法へと移行した点です。

  1. pause() 関数の削除:

    • 元の pause() 関数は、sys.Gosched() を繰り返し呼び出すことで、Goスケジューラに他のゴルーチンに実行を譲るように促していました。しかし、これはあくまで「ヒント」であり、OSスレッドのスケジューリングが関与する環境では、その効果が保証されませんでした。テストのタイミングが不安定になる原因となっていました。
  2. import "time"ticker の導入:

    • time パッケージのインポートにより、時間ベースの操作が可能になりました。
    • var ticker = time.Tick(10*1000) は、10マイクロ秒ごとにイベントを発生させるチャネル ticker を作成します。これは、一定の時間間隔を待つための信頼性の高い手段を提供します。
  3. sleep() 関数の新しい実装:

    • sleep() 関数は、<-ticker; <-ticker; によって少なくとも20マイクロ秒の経過を待ちます。これにより、OSスケジューラが他のゴルーチンに切り替えるための十分な時間が確保されます。
    • その後の sys.Gosched() の呼び出しは、依然として協調的なスケジューリングのヒントとして機能しますが、時間的な保証は time.Tick によって提供されます。
  4. strobe チャネルによる明示的な同期:

    • receiver および sender ヘルパー関数に strobe chan bool 引数が追加されました。
    • これらのヘルパー関数は、チャネル操作(値の受信または送信)が完了した直後に strobe <- true を実行します。これにより、ヘルパーゴルーチンがそのタスクを完了したことを呼び出し元(main 関数)に明示的に通知します。
    • main 関数では、var sync = make(chan bool) というバッファなしチャネルが作成され、これが strobe として各ヘルパーゴルーチンに渡されます。
    • main 関数は、ヘルパーゴルーチンを起動した後、<-sync を使用して、そのゴルーチンが完了通知を送信するまでブロックします。これにより、テストの各ステップが確実に完了してから次のステップに進むことが保証され、テストの実行順序とタイミングが厳密に制御されます。
  5. テストロジックの堅牢化:

    • 例えば、go i32receiver(c32, sync); sleep(); ok = c32 <- 123; if !ok { panic(...) } <-sync; のシーケンスでは、
      1. i32receiver ゴルーチンを起動します。
      2. sleep() で少し待機し、スケジューラが i32receiver を実行する機会を与えます。
      3. c32 <- 123 で値を送信します。
      4. <-synci32receiver が値を受け取り、strobe <- true を実行するのを待ちます。
    • この明示的な同期により、GoランタイムがどのようにゴルーチンをOSスレッドにマッピングし、スケジューリングするかに関わらず、テストの各段階が期待通りに進行することが保証されます。特に、バッファなしチャネルの場合、送信と受信が同時に行われる必要があるため、この同期は不可欠です。バッファ付きチャネルの場合でも、送信が完了したことを確認するために <-sync が使用されます。

この変更は、Go言語の初期段階におけるランタイムの進化と、並行処理テストの堅牢性を確保するための重要なステップを示しています。協調的スケジューリングの限界を認識し、より信頼性の高い時間ベースおよび明示的なチャネル同期メカニズムを導入することで、テストが様々な実行環境で安定して動作するように改善されています。

関連リンク

  • Go言語の並行処理: https://go.dev/doc/effective_go#concurrency
  • Goスケジューラに関する議論 (歴史的背景): Goの初期のスケジューラに関する公式ドキュメントやブログ記事は、Goが公開される前の2009年時点では限られていますが、Goの設計思想や進化を理解する上で重要です。

参考にした情報源リンク

  • Go言語公式ドキュメント: https://go.dev/doc/
  • Go言語のソースコード (特に runtime パッケージの歴史): https://github.com/golang/go
  • Goのチャネルに関する解説記事 (一般的な情報): https://go.dev/blog/pipelines など
  • time パッケージのドキュメント: https://pkg.go.dev/time
  • sys.Gosched() の歴史的背景については、Goの初期のコミットログや設計ドキュメントを追う必要がありますが、一般的にはGoのスケジューラの進化とともにその役割が変化していきました。# [インデックス 1549] ファイルの概要

このコミットは、Go言語のテストスイート内の test/chan/nonblock.go ファイルに対する変更です。具体的には、チャネルの非ブロッキング動作をテストする際に、Goランタイムが実際のOSスレッドを使用する環境下でもテストが正しく機能するように修正が加えられています。

コミット

commit d2117ad43874c6729ee532da1f73ddfa7ab2ed46
Author: Russ Cox <rsc@golang.org>
Date:   Fri Jan 23 17:04:56 2009 -0800

    make test/chan/nonblock work even with real os threads
    
    R=ken
    OCL=23422
    CL=23422

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d2117ad43874c6729ee532da1f73ddfa7ab2ed46

元コミット内容

make test/chan/nonblock work even with real os threads

このコミットメッセージは、test/chan/nonblock テストが「実際のOSスレッド」を使用する環境でも動作するように修正されたことを示しています。これは、Goランタイムのスケジューリングモデルが進化し、より直接的にOSスレッドを利用するようになったことへの対応と考えられます。

変更の背景

Go言語の初期のバージョンでは、ゴルーチン(Goの軽量スレッド)のスケジューリングは、Goランタイムが管理するM:Nスケジューリングモデル(M個のゴルーチンをN個のOSスレッドにマッピングする)に基づいていました。このモデルでは、ゴルーチンは協調的に(sys.Gosched()のような関数を呼び出すことで)CPUを譲り渡すことが期待される場合がありました。

しかし、実際のOSスレッドが関与するようになると、協調的なスケジューリングだけでは不十分になる場合があります。特に、チャネルの非ブロッキング操作のような時間依存のテストでは、ゴルーチンの実行順序やタイミングがOSスレッドのスケジューリングに影響される可能性が出てきます。

このコミットが行われた2009年1月は、Go言語がまだ公開される前の非常に初期の段階です。この時期は、Goランタイムのスケジューラや並行処理モデルが活発に開発・改善されていた時期にあたります。test/chan/nonblock.go は、チャネルの非ブロッキング送受信が期待通りに動作するかを検証するためのテストであり、Goの並行処理の根幹に関わる重要なテストです。

「real os threads」という記述は、GoランタイムがOSスレッドをより積極的に利用するようになった、あるいはその利用方法が変更されたことを示唆しています。これにより、テストのタイミングが変わり、従来の pause() 関数(内部で sys.Gosched() を複数回呼び出す)だけでは、テストが不安定になったり、失敗するようになった可能性があります。この変更は、Goランタイムの進化に合わせて、テストの堅牢性を高めることを目的としています。

前提知識の解説

Goのゴルーチンとチャネル

  • ゴルーチン (Goroutines): Go言語における軽量な実行スレッドです。OSスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行できます。Goランタイムがゴルーチンのスケジューリング、スタックの管理、OSスレッドへのマッピングを行います。
  • チャネル (Channels): ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは、Goの並行処理モデルであるCSP (Communicating Sequential Processes) の中心的な要素です。チャネルにはバッファリングされたものとされていないものがあり、それぞれ異なるブロッキング特性を持ちます。
    • バッファなしチャネル: 送信側と受信側が同時に準備ができていないと、どちらかの操作がブロックされます。
    • バッファありチャネル: バッファが満杯でない限り送信はブロックされず、バッファが空でない限り受信はブロックされません。

GoスケジューラとOSスレッド

Goランタイムには、ゴルーチンをOSスレッドにマッピングし、実行を管理するスケジューラが組み込まれています。初期のGoスケジューラは、ゴルーチンが明示的に sys.Gosched() を呼び出すことで、他のゴルーチンにCPUを譲る「協調的スケジューリング」の側面を持っていました。しかし、現代のGoスケジューラは、よりプリエンプティブ(横取り型)なスケジューリングに進化しており、ゴルーチンが長時間CPUを占有することを防ぎ、より公平な実行を保証しています。

sys.Gosched()

sys.Gosched() は、現在のゴルーチンがCPUを他のゴルーチンに譲ることをスケジューラに示唆する関数です。これは協調的マルチタスクの典型的な例であり、ゴルーチンが自発的に実行を中断し、他のゴルーチンに実行機会を与えるために使用されました。しかし、この方法は、ゴルーチンが Gosched を呼び出さないと他のゴルーチンが実行されない可能性があるため、デッドロックやパフォーマンスの問題を引き起こす可能性がありました。

time.Tick

time.Tick は、指定された期間ごとにイベントを発生させるチャネルを返します。これは、定期的な処理やタイムアウトの実装によく使用されます。このコミットでは、time.Tick を使用して、より信頼性の高い時間ベースの待機メカニズムを導入し、sys.Gosched() のみによる協調的な待機を置き換えています。

技術的詳細

このコミットの主要な変更点は、test/chan/nonblock.go テストにおけるゴルーチンの同期と待機メカニズムの改善です。

  1. pause() 関数の削除:

    • 元のコードでは、pause() 関数が for i:=0; i<100; i++ { sys.Gosched() } というループで sys.Gosched() を100回呼び出すことで、他のゴルーチンに実行機会を与えようとしていました。
    • この方法は、Goランタイムが「実際のOSスレッド」を使用するようになった環境では、その効果が不安定になる可能性がありました。OSスレッドのスケジューリングはGoランタイムの制御外であり、sys.Gosched() を呼び出しても、必ずしも期待通りに他のゴルーチンがすぐに実行されるとは限りません。
  2. import "time" の追加と ticker の導入:

    • import "time" が追加され、var ticker = time.Tick(10*1000); // 10 us が定義されました。これは、10マイクロ秒ごとに値を送信するチャネルを作成します。
  3. sleep() 関数の導入:

    • 新しい sleep() 関数は、<-ticker; <-ticker; sys.Gosched(); sys.Gosched(); sys.Gosched(); と実装されています。
    • <-ticker; を2回呼び出すことで、少なくとも20マイクロ秒の時間が経過するのを待ちます。これにより、OSスケジューラが他のゴルーチンに実行を切り替えるための十分な時間的余裕が生まれます。
    • その後の sys.Gosched() の呼び出しは、念のための協調的スケジューリングのヒントとして残されていますが、time.Tick による時間経過の保証が主要な待機メカニズムとなっています。
  4. strobe チャネルによるゴルーチン間の明示的な同期:

    • i32receiver, i32sender, i64receiver, i64sender, breceiver, bsender, sreceiver, ssender といったチャネル操作を行うヘルパー関数に、新たに strobe chan bool という引数が追加されました。
    • これらのヘルパー関数は、チャネル操作が完了した後に strobe <- true を実行し、完了を通知するようになりました。
    • main 関数では、var sync = make(chan bool) というバッファなしチャネルが導入され、この sync チャネルが strobe として各ヘルパー関数に渡されます。
    • これにより、main 関数は <-sync を使用して、ヘルパーゴルーチンがチャネル操作を完了したことを明示的に待つことができるようになりました。これは、テストの実行順序とタイミングをより厳密に制御するために非常に重要です。
  5. テストロジックの変更:

    • main 関数内のテストループでは、go i32receiver(c32, sync); sleep(); のように、ゴルーチンを起動した後に sleep() を呼び出し、その後 ok = c32 <- 123; のようにチャネル送信を行います。
    • そして、<-sync を呼び出すことで、受信側ゴルーチンが値を受け取ったことを確認します。
    • 送信側ゴルーチンについても同様に、go i32sender(c32, sync); の後に if buffer > 0 { <-sync } else { sleep() } のような条件付きの同期が追加されています。バッファ付きチャネルの場合は送信がブロックされないためすぐに <-sync で完了を待ち、バッファなしチャネルの場合は sleep() で少し待ってから受信側が値を受け取るのを待ちます。
    • これらの変更により、テストはゴルーチンの実行とチャネル操作の完了をより確実に同期できるようになり、OSスレッドのスケジューリングの変動に左右されにくくなりました。

コアとなるコードの変更箇所

--- a/test/chan/nonblock.go
+++ b/test/chan/nonblock.go
@@ -9,40 +9,55 @@
 
 package main
 
-func pause() {
-	for i:=0; i<100; i++ { sys.Gosched() }
-}
+import "time"
 
-func i32receiver(c chan int32) {
+func i32receiver(c chan int32, strobe chan bool) {
 	if <-c != 123 { panic("i32 value") }
+	strobe <- true
 }
 
-func i32sender(c chan int32) {
-	c <- 234
+func i32sender(c chan int32, strobe chan bool) {
+	c <- 234;
+	strobe <- true
 }
 
-func i64receiver(c chan int64) {
+func i64receiver(c chan int64, strobe chan bool) {
 	if <-c != 123456 { panic("i64 value") }
+	strobe <- true
 }
 
-func i64sender(c chan int64) {
-	c <- 234567
+func i64sender(c chan int64, strobe chan bool) {
+	c <- 234567;
+	strobe <- true
 }
 
-func breceiver(c chan bool) {
+func breceiver(c chan bool, strobe chan bool) {
 	if ! <-c { panic("b value") }
+	strobe <- true
 }
 
-func bsender(c chan bool) {
-	c <- true
+func bsender(c chan bool, strobe chan bool) {
+	c <- true;
+	strobe <- true
 }
 
-func sreceiver(c chan string) {
+func sreceiver(c chan string, strobe chan bool) {
 	if <-c != "hello" { panic("s value") }
+	strobe <- true
+}
+
+func ssender(c chan string, strobe chan bool) {
+	c <- "hello again";
+	strobe <- true
 }
 
-func ssender(c chan string) {
-	c <- "hello again"
+var ticker = time.Tick(10*1000);	// 10 us
+func sleep() {
+	<-ticker;
+	<-ticker;
+	sys.Gosched();
+	sys.Gosched();
+	sys.Gosched();
 }
 
  func main() {
@@ -52,45 +67,57 @@ func main() {
  	var s string;
  	var ok bool;
 
+	var sync = make(chan bool);
+
  	for buffer := 0; buffer < 2; buffer++ {
  	\tc32 := make(chan int32, buffer);
  	\tc64 := make(chan int64, buffer);
@@ -70,45 +87,57 @@ func main() {
  	\ts, ok = <-cs;
  	\tif ok { panic("blocked ssender") }
 
-\t\tgo i32receiver(c32);
-\t\tpause();
+\t\tgo i32receiver(c32, sync);
+\t\tsleep();
  	\tok = c32 <- 123;
-\t\tif !ok { panic("i32receiver") }\n-\t\tgo i32sender(c32);\n-\t\tpause();
+\t\tif !ok { panic("i32receiver buffer=", buffer) }
+\t\t<-sync;
+\n+\t\tgo i32sender(c32, sync);
+\t\tif buffer > 0 { <-sync } else { sleep() }
  	\ti32, ok = <-c32;
-\t\tif !ok { panic("i32sender") }
+\t\tif !ok { panic("i32sender buffer=", buffer) }
  	\tif i32 != 234 { panic("i32sender value") }
+\t\tif buffer == 0 { <-sync }
 
-\t\tgo i64receiver(c64);
-\t\tpause();
+\t\tgo i64receiver(c64, sync);
+\t\tsleep();
  	\tok = c64 <- 123456;
  	\tif !ok { panic("i64receiver") }
-\t\tgo i64sender(c64);\n-\t\tpause();
+\t\t<-sync;
+\n+\t\tgo i64sender(c64, sync);
+\t\tif buffer > 0 { <-sync } else { sleep() }
  	\ti64, ok = <-c64;
  	\tif !ok { panic("i64sender") }
  	\tif i64 != 234567 { panic("i64sender value") }
+\t\tif buffer == 0 { <-sync }
 
-\t\tgo breceiver(cb);
-\t\tpause();
+\t\tgo breceiver(cb, sync);
+\t\tsleep();
  	\tok = cb <- true;
  	\tif !ok { panic("breceiver") }
-\t\tgo bsender(cb);\n-\t\tpause();
+\t\t<-sync;
+\n+\t\tgo bsender(cb, sync);
+\t\tif buffer > 0 { <-sync } else { sleep() }
  	\tb, ok = <-cb;
  	\tif !ok { panic("bsender") }
  	\tif !b{ panic("bsender value") }
+\t\tif buffer == 0 { <-sync }
 
-\t\tgo sreceiver(cs);
-\t\tpause();
+\t\tgo sreceiver(cs, sync);
+\t\tsleep();
  	\tok = cs <- "hello";
  	\tif !ok { panic("sreceiver") }
-\t\tgo ssender(cs);\n-\t\tpause();
+\t\t<-sync;
+\n+\t\tgo ssender(cs, sync);
+\t\tif buffer > 0 { <-sync } else { sleep() }
  	\ts, ok = <-cs;
  	\tif !ok { panic("ssender") }
  	\tif s != "hello again" { panic("ssender value") }
+\t\tif buffer == 0 { <-sync }
  	}\n  	print("PASS\\n")
  }

コアとなるコードの解説

このコミットの核となる変更は、test/chan/nonblock.go テストの同期メカニズムを、協調的な sys.Gosched() ベースの pause() から、time.Tick と明示的なチャネル同期 (strobe/sync チャネル) を組み合わせたより堅牢な方法へと移行した点です。

  1. pause() 関数の削除:

    • 元の pause() 関数は、sys.Gosched() を繰り返し呼び出すことで、Goスケジューラに他のゴルーチンに実行を譲るように促していました。しかし、これはあくまで「ヒント」であり、OSスレッドのスケジューリングが関与する環境では、その効果が保証されませんでした。テストのタイミングが不安定になる原因となっていました。
  2. import "time"ticker の導入:

    • time パッケージのインポートにより、時間ベースの操作が可能になりました。
    • var ticker = time.Tick(10*1000) は、10マイクロ秒ごとにイベントを発生させるチャネル ticker を作成します。これは、一定の時間間隔を待つための信頼性の高い手段を提供します。
  3. sleep() 関数の新しい実装:

    • sleep() 関数は、<-ticker; <-ticker; によって少なくとも20マイクロ秒の経過を待ちます。これにより、OSスケジューラが他のゴルーチンに切り替えるための十分な時間が確保されます。
    • その後の sys.Gosched() の呼び出しは、依然として協調的なスケジューリングのヒントとして機能しますが、時間的な保証は time.Tick によって提供されます。
  4. strobe チャネルによる明示的な同期:

    • receiver および sender ヘルパー関数に strobe chan bool 引数が追加されました。
    • これらのヘルパー関数は、チャネル操作(値の受信または送信)が完了した直後に strobe <- true を実行します。これにより、ヘルパーゴルーチンがそのタスクを完了したことを呼び出し元(main 関数)に明示的に通知します。
    • main 関数では、var sync = make(chan bool) というバッファなしチャネルが作成され、これが strobe として各ヘルパーゴルーチンに渡されます。
    • main 関数は、ヘルパーゴルーチンを起動した後、<-sync を使用して、そのゴルーチンが完了通知を送信するまでブロックします。これにより、テストの各ステップが確実に完了してから次のステップに進むことが保証され、テストの実行順序とタイミングが厳密に制御されます。
  5. テストロジックの堅牢化:

    • 例えば、go i32receiver(c32, sync); sleep(); ok = c32 <- 123; if !ok { panic(...) } <-sync; のシーケンスでは、
      1. i32receiver ゴルーチンを起動します。
      2. sleep() で少し待機し、スケジューラが i32receiver を実行する機会を与えます。
      3. c32 <- 123 で値を送信します。
      4. <-synci32receiver が値を受け取り、strobe <- true を実行するのを待ちます。
    • この明示的な同期により、GoランタイムがどのようにゴルーチンをOSスレッドにマッピングし、スケジューリングするかに関わらず、テストの各段階が期待通りに進行することが保証されます。特に、バッファなしチャネルの場合、送信と受信が同時に行われる必要があるため、この同期は不可欠です。バッファ付きチャネルの場合でも、送信が完了したことを確認するために <-sync が使用されます。

この変更は、Go言語の初期段階におけるランタイムの進化と、並行処理テストの堅牢性を確保するための重要なステップを示しています。協調的スケジューリングの限界を認識し、より信頼性の高い時間ベースおよび明示的なチャネル同期メカニズムを導入することで、テストが様々な実行環境で安定して動作するように改善されています。

関連リンク

  • Go言語の並行処理: https://go.dev/doc/effective_go#concurrency
  • Goスケジューラに関する議論 (歴史的背景): Goの初期のスケジューラに関する公式ドキュメントやブログ記事は、Goが公開される前の2009年時点では限られていますが、Goの設計思想や進化を理解する上で重要です。

参考にした情報源リンク

  • Go言語公式ドキュメント: https://go.dev/doc/
  • Go言語のソースコード (特に runtime パッケージの歴史): https://github.com/golang/go
  • Goのチャネルに関する解説記事 (一般的な情報): https://go.dev/blog/pipelines など
  • time パッケージのドキュメント: https://pkg.go.dev/time
  • sys.Gosched() の歴史的背景については、Goの初期のコミットログや設計ドキュメントを追う必要がありますが、一般的にはGoのスケジューラの進化とともにその役割が変化していきました。