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

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

このコミットは、Go言語のランタイムにおける select ステートメントの default ケースのサポートを追加するものです。具体的には、チャネル操作が準備できていない場合に即座に実行される default ブロックの挙動を、ランタイムレベルで実現するための変更が含まれています。

コミット

commit b69e80d8dd683600c70a334da52fc0cd8a56e739
Author: Russ Cox <rsc@golang.org>
Date:   Wed Nov 5 17:57:18 2008 -0800

    runtime support for default in select.
    assumes cas->send == 2 for default case.
    
    R=ken
    OCL=18628
    CL=18628

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

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

元コミット内容

このコミットは、Go言語の select ステートメントにおける default ケースのランタイムサポートを導入します。default ケースは、Scase 構造体の send フィールドが 2 であると仮定して処理されます。

変更の背景

Go言語の select ステートメントは、複数のチャネル操作を待機し、準備ができた最初の操作を実行するための強力な並行処理プリミティブです。しかし、初期のGo言語では、select が常にブロックする挙動しか持たず、チャネル操作が準備できていない場合に即座に処理を続行する非ブロッキングな select を実現する方法がありませんでした。

このコミットが行われた2008年11月は、Go言語がまだ一般に公開される前の開発初期段階にあたります。この時期に、select ステートメントに default ケースの概念を導入することで、開発者はチャネル操作が利用可能でない場合に、別の処理を実行したり、タイムアウトを実装したりするなど、より柔軟な並行処理パターンを記述できるようになりました。これは、Goの並行処理モデルをより実用的で表現豊かなものにするための重要なステップでした。

前提知識の解説

Go言語の並行処理とチャネル

Go言語は、CSP (Communicating Sequential Processes) に基づく並行処理モデルを採用しています。これは、共有メモリによる同期ではなく、チャネルを通じたゴルーチン間の通信によって並行処理を実現するという考え方です。

  • ゴルーチン (Goroutine): Goランタイムによって管理される軽量なスレッドのようなものです。数千、数万のゴルーチンを同時に実行してもオーバーヘッドが少ないのが特徴です。
  • チャネル (Channel): ゴルーチン間で値を送受信するための通信路です。チャネルは型付けされており、make(chan Type) で作成します。チャネルへの送信 (ch <- value) や受信 (value <- ch) は、デフォルトでブロッキング操作です。

select ステートメント

select ステートメントは、複数のチャネル操作を同時に待機し、そのうちのいずれかが準備できた場合に、その操作に対応するケースを実行します。

select {
case <-ch1:
    // ch1 から値を受信
case ch2 <- value:
    // ch2 へ値を送信
case <-time.After(5 * time.Second):
    // 5秒後にタイムアウト
default:
    // どのチャネル操作も準備できていない場合に実行
}

select は以下のルールで動作します。

  1. 複数の case が同時に準備できた場合、ランダムに1つが選択されます。
  2. どの case も準備できていない場合、default ケースが存在すればそれが実行されます。
  3. default ケースが存在せず、どの case も準備できていない場合、select ステートメントはチャネル操作のいずれかが準備できるまでブロックします。

このコミットは、上記の「2. default ケースが存在すればそれが実行されます」という挙動をランタイムレベルでサポートするためのものです。

Goランタイムの内部構造 (初期のGo)

Go言語のランタイムは、Goプログラムの実行を管理するC言語(およびアセンブリ言語)で書かれた部分です。チャネル、スケジューラ、ガベージコレクタなどの低レベルな機能は、このランタイムによって提供されます。

  • src/runtime/chan.c: このファイルは、Goのチャネルの基本的な操作(作成、送信、受信、クローズなど)をC言語で実装しています。select ステートメントの内部処理もこのファイルに含まれています。
  • Scase 構造体: select ステートメント内の個々の case を表現するためにランタイムが使用する内部構造体です。各 case がどのチャネルを参照しているか、送信操作か受信操作か、などの情報を含みます。

技術的詳細

このコミットの主要な変更は、src/runtime/chan.c ファイル内の Scase 構造体と sys·selectgo 関数にあります。

Scase 構造体の拡張

Scase 構造体は、select ステートメント内の各 case を表現するためにランタイムが使用する内部データ構造です。この構造体には、チャネルへのポインタ、リターンPC(プログラムカウンタ)、そして操作の種類を示す send フィールドが含まれています。

変更前は、send フィールドは以下の意味を持っていました。

  • 0: 受信操作 (receive)
  • 1: 送信操作 (send)

このコミットでは、default ケースを識別するために、send フィールドに新しい値 2 が導入されました。これにより、ランタイムは Scase インスタンスが通常のチャネル操作ではなく、default ケースを表していることを認識できるようになります。

sys·selectgo 関数の変更

sys·selectgo 関数は、Goの select ステートメントがコンパイル時に呼び出すランタイム関数です。この関数は、select ステートメント内のすべての case を評価し、適切なチャネル操作を実行するか、default ケースがあればそれを実行する役割を担います。

変更の核心は、sys·selectgodefault ケースを特別に処理するように修正された点です。

  1. default ケースの識別と分離: 関数はまず、select ステートメント内のすべての case をループで走査します。この際、cas->send == 2 である Scase インスタンス(つまり default ケース)が見つかった場合、その Scase へのポインタを dfl という新しい変数に保存し、現在のループイテレーションをスキップします。これにより、最初のパスでは通常のチャネル操作のみが評価され、default ケースは一時的に除外されます。

  2. 非ブロッキング評価と default へのフォールバック: 最初のパスでは、各チャネルが既に準備ができているか(例:バッファ付きチャネルにデータがある、または受信者が待機している)がチェックされます。もし準備ができているチャネル操作が見つかれば、その操作が選択され、select は終了します。 しかし、最初のパスでどのチャネル操作も準備ができていなかった場合、select は通常、チャネル操作が準備できるまでブロックします。このコミットでは、このブロッキングの前に dfl != nil (つまり default ケースが存在する) かどうかをチェックするロジックが追加されました。

  3. default ケースの実行: もし default ケースが存在し、かつ他のどのチャネル操作も即座に準備できていなかった場合、cas ポインタは dfl に設定され、goto retc; ステートメントによって、select の結果を返す共通の処理パスにジャンプします。これにより、default ケースが非ブロッキングで実行されることが保証されます。

このメカニズムにより、select はまず非ブロッキングでチャネルを試行し、どれも成功しない場合にのみ default ケースにフォールバックするという、Go言語の select の期待される挙動が実現されました。

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

--- a/src/runtime/chan.c
+++ b/src/runtime/chan.c
@@ -52,7 +52,7 @@ struct	Scase
 {
 	Hchan*	chan;			// chan
 	byte*\tpc;			// return pc
-	uint16	send;			// 0-recv 1-send
+	uint16	send;			// 0-recv 1-send 2-default
 	uint16	so;			// vararg of selected bool
 	union {
 		byte	elem[8];	// element (send)
@@ -504,7 +504,7 @@ void
 sys·selectgo(Select *sel)
 {
 	uint32 p, o, i;
-	Scase *cas;
+	Scase *cas, *dfl;
 	Hchan *c;
 	SudoG *sg;
 	G *gp;
@@ -542,8 +542,13 @@ sys·selectgo(Select *sel)
 	lock(&chanlock);
 
 	// pass 1 - look for something already waiting
+	dfl = nil;
 	for(i=0; i<sel->ncase; i++) {
 		cas = &sel->scase[o];
+		if(cas->send == 2) {	// default
+			dfl = cas;
+			continue;
+		}
 		c = cas->chan;
 		if(c->dataqsiz > 0) {
 			if(cas->send) {
@@ -569,6 +574,12 @@ sys·selectgo(Select *sel)
 		if(o >= sel->ncase)
 			o -= sel->ncase;
 	}
+	
+	if(dfl != nil) {
+		cas = dfl;
+		goto retc;
+	}
+		
 
 	// pass 2 - enqueue on all chans
 	for(i=0; i<sel->ncase; i++) {

コアとなるコードの解説

struct Scase の変更

-	uint16	send;			// 0-recv 1-send
+	uint16	send;			// 0-recv 1-send 2-default

Scase 構造体の send フィールドのコメントが更新され、2-default という新しい値が追加されました。これは、send フィールドが 2 の場合、その Scase インスタンスが select ステートメントの default ケースを表すことを明示しています。これにより、ランタイムは default ケースを他のチャネル操作と区別できるようになります。

sys·selectgo 関数の変更

 	uint32 p, o, i;
-	Scase *cas;
+	Scase *cas, *dfl; // dfl (default) ポインタが追加
 	Hchan *c;
 	SudoG *sg;
 	G *gp;

sys·selectgo 関数内に、Scase *dfl; という新しいポインタ変数が宣言されました。この dfl ポインタは、select ステートメント内に default ケースが存在する場合に、その Scase インスタンスを指すために使用されます。

 	lock(&chanlock);
 
 	// pass 1 - look for something already waiting
+	dfl = nil; // dfl を nil で初期化
 	for(i=0; i<sel->ncase; i++) {
 		cas = &sel->scase[o];
+		if(cas->send == 2) {	// default
+			dfl = cas; // default ケースが見つかったら dfl に保存
+			continue;  // このパスでは default ケースをスキップ
+		}
 		c = cas->chan;
 		if(c->dataqsiz > 0) {
 			if(cas->send) {

sys·selectgo の最初のループ (pass 1) は、既に準備ができているチャネル操作を探します。このループの冒頭で dfl = nil; と初期化されます。 ループ内で、現在の cas (select case) の send フィールドが 2 であるかどうかがチェックされます。もし 2 であれば、それは default ケースであるため、その casdfl ポインタに保存し、continue で現在のイテレーションをスキップします。これにより、default ケースは最初の「準備ができているチャネルを探す」パスでは評価されず、後で特別な処理が施されることになります。

 	// ... (pass 1 の残りのロジック) ...

 	// pass 1 で何も見つからなかった場合、このブロックが実行される
+	if(dfl != nil) { // default ケースが存在するかチェック
+		cas = dfl;   // 存在すれば cas を default ケースに設定
+		goto retc;   // retc (リターン処理) へジャンプ
+	}
+		
 
 	// pass 2 - enqueue on all chans (チャネルが準備できるまでブロックする処理)
 	for(i=0; i<sel->ncase; i++) {

最初のループ (pass 1) が終了した後、もしどのチャネル操作も即座に準備できていなかった場合、この新しいブロックが実行されます。 if(dfl != nil) は、select ステートメントに default ケースが含まれていたかどうかをチェックします。 もし dflnil でなければ(つまり default ケースが存在すれば)、cas ポインタを dfl が指す default ケースに設定し、goto retc; を使用して関数の終了処理(select の結果を返す部分)に直接ジャンプします。 この goto により、select はチャネルが準備できるまでブロックすることなく、直ちに default ケースの処理を実行し、sys·selectgo 関数から戻ることができます。これは、selectdefault ケースが非ブロッキングであるという重要な特性を実現しています。

関連リンク

参考にした情報源リンク

  • Go言語の初期の設計に関する議論やメーリングリストのアーカイブ(Go言語がオープンソース化された後の情報が主だが、初期の設計思想を理解する上で参考になる場合がある)
  • Go言語のランタイムに関する技術ブログや解説記事(chan.c のような低レベルな実装を解説しているもの)
  • CSP (Communicating Sequential Processes) に関する文献(Goの並行処理モデルの基礎)