[インデックス 1070] ファイルの概要
このコミットは、Go言語のランタイムにおけるチャネルのselect
ステートメント、特にdefault
ケースのサポートを強化するためのものです。src/runtime/chan.c
ファイルに、select
のdefault
ケースを処理するための新しいランタイム関数sys·selectdefault
が追加されました。
コミット
commit a6af48432819fcc6c963d1ac883aefe8cd6f7555
Author: Russ Cox <rsc@golang.org>
Date: Wed Nov 5 18:04:24 2008 -0800
more runtime support for chan select default
R=ken
OCL=18630
CL=18630
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a6af48432819fcc6c963d1ac883aefe8cd6f7555
元コミット内容
このコミットの元のメッセージは「more runtime support for chan select default」であり、Go言語のチャネルにおけるselect
ステートメントのdefault
ケースに対するランタイムサポートをさらに追加する意図が示されています。
変更の背景
Go言語のselect
ステートメントは、複数のチャネル操作を待機し、準備ができた最初の操作を実行するために使用されます。select
ステートメントにはオプションでdefault
ケースを含めることができ、これはどのチャネル操作もすぐに実行できない場合に即座に実行されます。default
ケースがない場合、select
はチャネル操作のいずれかが準備できるまでブロックします。
このコミットが行われた2008年当時、Go言語はまだ開発の初期段階にあり、ランタイムの様々なコンポーネントが構築されていました。select
ステートメントのdefault
ケースは、ノンブロッキングなチャネル操作を実現するために不可欠な機能です。このコミットは、コンパイラが生成するコードとランタイムが連携してselect
のdefault
ケースを正しく処理できるように、ランタイム側のサポートを強化することを目的としています。
具体的には、select
ステートメントがdefault
ケースを持つ場合、ランタイムは他のチャネル操作が準備できていないことを検出した際に、このdefault
ケースにジャンプする必要があります。このコミットで追加されたsys·selectdefault
関数は、このdefault
ケースの情報をランタイムのSelect
構造体に登録し、select
の実行ロジックがdefault
ケースを適切に選択できるようにするためのものです。
また、既存のsys·selectrecv
関数内のエラーメッセージの修正も行われています。これは、selectsend
(送信)ではなくselectrecv
(受信)のコンテキストで「too many cases」というエラーが発生した場合に、より正確なメッセージをスローするようにするためのバグ修正と考えられます。
前提知識の解説
Go言語のチャネル (Channels)
Go言語のチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送信できます。チャネルは、ゴルーチン間の同期と通信を安全に行うための主要な手段であり、共有メモリによる競合状態を避けるために設計されています。
- チャネルの作成:
make(chan int)
のように作成します。 - 送信:
ch <- value
のようにチャネルに値を送信します。 - 受信:
value := <-ch
のようにチャネルから値を受信します。
select
ステートメント
select
ステートメントは、複数のチャネル操作を同時に待機し、準備ができた最初の操作を実行するために使用されます。これは、Unixのselect
システムコールに似た概念で、複数のI/O操作を監視するのに使われます。
select
の基本的な構文は以下の通りです。
select {
case <-ch1:
// ch1からの受信が準備できた場合
case ch2 <- value:
// ch2への送信が準備できた場合
default:
// どのチャネル操作もすぐに準備できていない場合
}
select
のdefault
ケース
select
ステートメントにdefault
ケースが含まれている場合、select
はどのチャネル操作もすぐに実行できない(つまり、チャネルがブロックされる)場合に、default
ケースのコードブロックを即座に実行します。これにより、select
ステートメント全体がノンブロッキングになります。default
ケースがない場合、select
はチャネル操作のいずれかが準備できるまでブロックします。
Goランタイムの内部構造(初期のGo)
Go言語のランタイムは、ゴルーチンのスケジューリング、チャネル操作、ガベージコレクションなど、Goプログラムの実行を管理する低レベルのコードです。初期のGoランタイムは主にC言語で書かれており、チャネル操作のようなプリミティブはsrc/runtime/chan.c
のようなファイルで実装されていました。
Select
構造体:select
ステートメントの内部状態を管理するためにランタイムが使用する構造体です。これには、各case
の情報(チャネル、操作の種類、関連するデータなど)が格納されます。Scase
構造体:Select
構造体の一部として、個々のcase
(チャネル操作)の詳細を保持します。これには、操作の種類(送信/受信/デフォルト)、関連するチャネル、データポインタなどが含まれます。pc
(Program Counter): プログラムカウンタ。特定のコードの実行位置を示します。ランタイムは、select
が完了した後にどのコードに戻るべきかをpc
で記録します。so
(Stack Offset): スタックオフセット。スタック上の変数の位置を示します。send
フィールド:Scase
構造体内のフィールドで、そのcase
が送信操作(1)、受信操作(0)、またはdefault
操作(2)であるかを示します。
技術的詳細
このコミットの主要な変更点は、src/runtime/chan.c
ファイルにsys·selectdefault
関数が追加されたことです。この関数は、Goコンパイラによって生成されたコードから呼び出され、select
ステートメントのdefault
ケースに関する情報をランタイムに登録します。
sys·selectdefault
関数の役割
sys·selectdefault
関数は、select
ステートメントにdefault
ケースが存在する場合に、そのdefault
ケースの情報をSelect
構造体(sel
)に設定します。
-
ケースのインデックス管理:
i = sel->ncase;
if(i >= sel->tcase) throw("selectdefault: too many cases");
sel->ncase = i+1;
これは、現在のselect
ステートメントで処理されているケースの数を追跡し、新しいdefault
ケースをsel->scase
配列の次の利用可能なスロットに割り当てます。sel->tcase
は、select
ステートメントが持つケースの総数(default
を含む)を示します。 -
Scase
構造体の初期化:cas = &sel->scase[i];
新しく割り当てられたScase
構造体へのポインタを取得します。 -
プログラムカウンタ (pc) の設定:
cas->pc = sys·getcallerpc(&sel);
sys·getcallerpc
は、この関数を呼び出した場所のプログラムカウンタ(リターンアドレス)を取得します。これは、select
ステートメントがdefault
ケースを実行した後に、Goプログラムのどこに戻るべきかをランタイムが知るために使用されます。 -
チャネル (chan) の設定:
cas->chan = c;
default
ケースは特定のチャネル操作に関連付けられていないため、c
はnil
(Goのnil
に相当)に設定されます。 -
スタックオフセット (so) の設定:
eo = rnd(sizeof(sel), sizeof(c));
eo = rnd(eo+sizeof(c), sizeof(byte*));
cas->so = rnd(eo+sizeof(byte*), 1);
これらの行は、スタック上の要素のオフセットを計算しています。rnd
関数はアライメントを考慮した丸めを行うユーティリティ関数です。cas->so
は、default
ケースに関連するデータ(もしあれば)がスタック上のどこにあるかを示すオフセットです。default
ケースでは通常、チャネル操作のようなデータ転送がないため、このオフセットは主にデバッグや内部的なアライメントのために使用される可能性があります。 -
操作の種類 (send) の設定:
cas->send = 2;
これは非常に重要です。Scase
構造体のsend
フィールドは、そのケースがどのような操作を表すかを示します。0
: 受信操作1
: 送信操作2
:default
ケース この設定により、ランタイムのselect
実行ロジックは、このケースがdefault
ケースであることを認識し、他のチャネル操作が準備できていない場合にこれを実行する候補として扱います。
-
要素ポインタ (u.elemp) の設定:
cas->u.elemp = *(byte**)((byte*)&sel + eo);
u.elemp
は、チャネル操作で送受信される要素へのポインタを格納するために使用される共用体(union)の一部です。default
ケースでは直接的な要素の送受信がないため、この行はスタック上の特定の場所を指すように設定されていますが、default
ケースの実行ロジックでは通常使用されません。これは、Scase
構造体の一般的な初期化の一部として行われる可能性があります。 -
デバッグ出力:
if(debug) { ... }
デバッグモードが有効な場合、新しく登録されたdefault
ケースに関する情報(Select
ポインタ、pc
、chan
、so
、send
)が標準出力にプリントされます。これは、ランタイムの動作をデバッグするために役立ちます。
エラーメッセージの修正
sys·selectrecv
関数内の以下の行が変更されました。
--- a/src/runtime/chan.c
+++ b/src/runtime/chan.c
@@ -469,7 +469,7 @@ sys·selectrecv(Select *sel, Hchan *c, ...)
i = sel->ncase;
if(i >= sel->tcase)
- throw("selectsend: too many cases");
+ throw("selectrecv: too many cases");
sel->ncase = i+1;
cas = &sel->scase[i];
これは、sys·selectrecv
(チャネルからの受信操作を登録する関数)内で、ケース数が多すぎる場合にスローされるエラーメッセージが誤ってselectsend
と表示されていたのを、正しいselectrecv
に修正したものです。これは小さなバグ修正ですが、ランタイムのエラーメッセージの正確性を向上させます。
コアとなるコードの変更箇所
src/runtime/chan.c
ファイルにおいて、以下の変更が行われました。
sys·selectrecv
関数内のエラーメッセージの修正(1行変更)。sys·selectdefault
関数の新規追加(39行追加)。
// 修正箇所
@@ -469,7 +469,7 @@ sys·selectrecv(Select *sel, Hchan *c, ...)
i = sel->ncase;
if(i >= sel->tcase)
- throw("selectsend: too many cases");
+ throw("selectrecv: too many cases");
sel->ncase = i+1;
cas = &sel->scase[i];
// 追加箇所
+void
+sys·selectdefault(Select *sel)
+{
+ int32 i, eo;
+ Scase *cas;
+ Hchan *c;
+
+ c = nil;
+ i = sel->ncase;
+ if(i >= sel->tcase)
+ throw("selectdefault: too many cases");
+ sel->ncase = i+1;
+ cas = &sel->scase[i];
+
+ cas->pc = sys·getcallerpc(&sel);
+ cas->chan = c;
+
+ eo = rnd(sizeof(sel), sizeof(c));
+ eo = rnd(eo+sizeof(c), sizeof(byte*));
+ cas->so = rnd(eo+sizeof(byte*), 1);
+ cas->send = 2;
+ cas->u.elemp = *(byte**)((byte*)&sel + eo);
+
+ if(debug) {
+ prints("newselect s=");
+ sys·printpointer(sel);
+ prints(" pc=");
+ sys·printpointer(cas->pc);
+ prints(" chan=");
+ sys·printpointer(cas->chan);
+ prints(" so=");
+ sys·printint(cas->so);
+ prints(" send=");
+ sys·printint(cas->send);
+ prints("\n");
+ }
+}
コアとなるコードの解説
sys·selectdefault
関数
この関数は、Goコンパイラがselect
ステートメントのdefault
ケースを検出した際に、ランタイムにその情報を登録するために呼び出されます。
Select *sel
: 現在のselect
ステートメントの全体の状態を保持するSelect
構造体へのポインタです。int32 i, eo;
:i
はScase
配列のインデックス、eo
はスタックオフセット計算用の一時変数です。Scase *cas;
: 現在処理しているdefault
ケースに対応するScase
構造体へのポインタです。Hchan *c;
: チャネルへのポインタですが、default
ケースでは特定のチャネルに関連しないためnil
に設定されます。
関数の主要なロジックは以下の通りです。
sel->ncase
をインクリメントし、sel->scase
配列の次の空きスロットにdefault
ケースを登録します。sel->tcase
を超過するとエラーをスローします。cas->pc = sys·getcallerpc(&sel);
:default
ケースが実行された後に制御が戻るべきプログラムカウンタ(呼び出し元のGoコードの場所)を記録します。cas->chan = c;
:default
ケースはチャネル操作ではないため、チャネルポインタはnil
に設定されます。cas->so = ...;
: スタックオフセットを計算し設定します。これは、default
ケースに関連するデータ(もしあれば)がスタック上のどこにあるかを示すためのものです。cas->send = 2;
: この行が最も重要です。send
フィールドに2
を設定することで、このScase
がdefault
ケースであることをランタイムに明示的に伝えます。ランタイムのselect
ロジックは、この値を見て、他のチャネル操作が準備できていない場合にdefault
ケースを実行する判断を下します。cas->u.elemp = ...;
:u.elemp
はチャネル操作で送受信される要素へのポインタを格納するために使用されますが、default
ケースでは直接的な要素の送受信がないため、一般的な初期化の一部として設定されます。if(debug)
ブロック: デバッグビルドの場合、登録されたdefault
ケースの詳細情報がプリントされ、ランタイムの動作を追跡するのに役立ちます。
このsys·selectdefault
関数が追加されたことで、Goコンパイラはselect
ステートメントにdefault
ケースがある場合にこの関数を呼び出すようになり、ランタイムはdefault
ケースの存在と位置を正確に把握できるようになりました。これにより、select
ステートメントのノンブロッキングな動作がランタイムレベルで適切にサポートされるようになりました。
関連リンク
- Go言語の
select
ステートメントに関する公式ドキュメント(現代のGo):https://go.dev/tour/concurrency/5 - Go言語のチャネルに関する公式ドキュメント:https://go.dev/tour/concurrency/2
- このコミットが属するGoリポジトリ:https://github.com/golang/go
参考にした情報源リンク
- Go言語のソースコード(特に
src/runtime/chan.c
の歴史的なバージョン) - Go言語の
select
ステートメントとチャネルに関する一般的な知識 - Go言語の初期のランタイム実装に関する議論やドキュメント(もし公開されていれば)
- Go言語の
select
ステートメントの内部動作に関する技術記事やブログポスト(一般的な概念理解のため) - Go言語の
select
ステートメントのdefault
ケースの動作に関する情報