[インデックス 1071] ファイルの概要
このコミットでは、Go言語のコンパイラ (gc
) とランタイム (runtime
) において、select
ステートメントに default
ケースのサポートが追加されています。具体的には、以下のファイルが変更されました。
src/cmd/gc/subr.c
:selectdefault
シンボルの認識を追加。src/cmd/gc/sys.go
:sys.selectdefault
関数をエクスポート。src/cmd/gc/sysimport.c
:sys.selectdefault
のインポート定義を追加。src/cmd/gc/walk.c
:select
ステートメントの構文解析とコード生成ロジックを更新し、default
ケースを処理するように変更。特に、selcase
関数とwalkselect
関数が修正されています。src/runtime/chan.c
:sys.selectdefault
ランタイム関数の実装を追加・修正。src/runtime/runtime.c
:rnd
関数の挙動を調整するためのmaxround
変数を追加。src/runtime/runtime.h
:maxround
変数の宣言を追加。
コミット
commit 79fbbe37a76502e6f5f9647d2d82bab953ab1546
Author: Ken Thompson <ken@golang.org>
Date: Wed Nov 5 21:50:28 2008 -0800
select default
R=r
OCL=18646
CL=18646
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/79fbbe37a76502e6f5f9647d2d82bab953ab1546
元コミット内容
select default
R=r
OCL=18646
CL=18646
変更の背景
Go言語の初期段階において、select
ステートメントはチャネル操作の多重化を可能にする重要な機能として導入されました。しかし、この時点では、どのチャネル操作も準備ができていない場合にブロックせずに処理を続行するメカニズム、すなわち default
ケースが存在しませんでした。
select
ステートメントに default
ケースがない場合、すべてのケースがブロックされると select
ステートメント全体がブロックされ、デッドロックや応答性の低下につながる可能性がありました。ノンブロッキングなチャネル操作や、タイムアウト処理、あるいはチャネルが準備できていない場合に別の処理を実行するといったユースケースに対応するためには、default
ケースの導入が不可欠でした。
このコミットは、Go言語の並行処理モデルをより柔軟かつ強力にするために、select
ステートメントに default
ケースの機能を追加することを目的としています。これにより、開発者はチャネル操作の準備状況に応じて、より複雑な並行処理ロジックを記述できるようになります。
前提知識の解説
Go言語の並行処理とチャネル
Go言語は、CSP (Communicating Sequential Processes) に基づく並行処理モデルを採用しています。このモデルの中心となるのが「ゴルーチン (goroutine)」と「チャネル (channel)」です。
- ゴルーチン (Goroutine): Goランタイムによって管理される軽量なスレッドのようなものです。数千、数万のゴルーチンを同時に実行してもオーバーヘッドが少ないのが特徴です。
go
キーワードを使って関数を呼び出すことで、新しいゴルーチンを起動できます。 - チャネル (Channel): ゴルーチン間で値を安全に送受信するための通信路です。チャネルは型付けされており、特定の型の値のみを送受信できます。チャネルへの送信 (
chan <- value
) や受信 (<- chan
) は、デフォルトでブロッキング操作です。つまり、送信側は受信側が値を受け取るまでブロックし、受信側は送信側が値を送るまでブロックします。これにより、ゴルーチン間の同期が自然に行われます。
select
ステートメント
select
ステートメントは、複数のチャネル操作を同時に待ち受けるためのGo言語の制御構造です。select
は、switch
ステートメントに似ていますが、評価されるのはチャネルの送受信操作です。
select
ステートメントの基本的な動作は以下の通りです。
select
内のすべてのcase
式(チャネルの送受信操作)が評価されます。- 準備ができている(ブロッキングせずに実行できる)
case
が複数ある場合、その中からランダムに1つが選択されて実行されます。 - 準備ができている
case
が1つだけの場合、そのcase
が実行されます。 default
ケースがない場合: どのcase
も準備ができていない場合、select
ステートメント全体がブロックされ、いずれかのcase
が準備できるまで待機します。default
ケースがある場合: どのcase
も準備ができていない場合、default
ケースがすぐに実行されます。これにより、select
ステートメントはブロッキングせずに続行できます。
default
ケースは、ノンブロッキングなチャネル操作を実現したり、タイムアウト処理を実装したりする際に非常に有用です。
select {
case <-ch1:
// ch1 から値を受信
case ch2 <- value:
// ch2 へ値を送信
default:
// どのチャネル操作も準備ができていない場合に実行
}
技術的詳細
このコミットは、Goコンパイラ (gc
) とランタイム (runtime
) の両方にわたる変更を含んでおり、select
ステートメントにおける default
ケースのサポートを統合的に実現しています。
コンパイラ (src/cmd/gc/
) 側の変更
-
シンボル認識 (
src/cmd/gc/subr.c
):isselect
関数は、特定のシンボルがselect
ステートメントに関連するかどうかを判断します。このコミットでは、selectdefault
という新しいシンボルが追加され、コンパイラがdefault
ケースを認識できるようにしています。これは、select
のdefault
ケースが内部的にsys.selectdefault
というランタイム関数呼び出しに変換されるためです。 -
システム関数のエクスポート (
src/cmd/gc/sys.go
):sys.go
ファイルは、GoコンパイラがGoプログラムから呼び出すことができる内部的なシステム関数を定義しています。export func selectdefault(sel *byte) (selected bool);
の行が追加され、selectdefault
ランタイム関数がコンパイラから利用可能になります。 -
システム関数のインポート定義 (
src/cmd/gc/sysimport.c
):sysimport.c
は、コンパイラがGoのソースコードを解析する際に、Goランタイムの内部関数をどのように認識するかを定義するC言語の文字列を含んでいます。export func sys.selectdefault (sel *uint8) (selected bool)\n
が追加され、selectdefault
関数がGoの組み込み関数として扱われるようになります。 -
select
ステートメントの構文解析とコード生成 (src/cmd/gc/walk.c
): このファイルは、GoのAST (Abstract Syntax Tree) を走査し、高レベルなGoの構文を低レベルな中間表現に変換する「ウォーク (walk)」処理を担当します。selcase
関数: 個々のselect
ケース(送受信またはdefault
)を処理します。default
ケース (n->left == N
の場合) が追加され、syslook("selectdefault", 0)
を使ってselectdefault
ランタイム関数への呼び出しを生成するように変更されています。walkselect
関数:select
ステートメント全体を処理します。def
という新しいNode
ポインタが追加され、default
ケースのノードを保持します。OXCASE
の処理において、n->left == N
(つまりdefault
ケース) のチェックが追加されました。default
ケースが複数ある場合にyyerror("only one default select allowed");
というエラーを発生させるロジックが追加されています。これは、Go言語の仕様としてselect
ステートメントにはdefault
ケースを1つしか持てないためです。- エラーメッセージも
select cases must be send or recv
からselect cases must be send, recv or default
に更新され、default
ケースが正当な選択肢であることを反映しています。
ランタイム (src/runtime/
) 側の変更
-
selectdefault
ランタイム関数の実装 (src/runtime/chan.c
):src/runtime/chan.c
は、Goのチャネル操作に関するランタイムロジックを実装しています。sys·selectdefault
関数が追加されました。この関数は、select
ステートメントのdefault
ケースが実行される際にGoランタイムによって呼び出されます。- この関数は、
Select
構造体(select
操作の状態を保持する内部構造体)にdefault
ケースの情報を登録します。具体的には、cas->chan = nil;
とcas->u.elemp = nil;
と設定することで、このケースが特定のチャネル操作ではないことを示し、cas->send = 2;
でdefault
ケースであることを識別します。 rnd
関数の呼び出しが修正され、cas->so
(オフセット) の計算がdefault
ケースの特性に合わせて調整されています。
-
rnd
関数の調整 (src/runtime/runtime.c
,src/runtime/runtime.h
):src/runtime/runtime.c
にint32 maxround = 8;
というグローバル変数が追加されました。rnd
関数は、アライメントを考慮してサイズを丸めるためのユーティリティ関数です。このコミットでは、rnd
関数内でm > maxround
の場合にm = maxround
とすることで、丸め処理の最大値を制限しています。これは、selectdefault
の引数処理におけるアライメント計算の安定性や効率性に関連している可能性があります。src/runtime/runtime.h
にextern int32 maxround;
が追加され、maxround
変数が他のランタイムファイルから参照可能になっています。
これらの変更により、Goコンパイラは select
ステートメント内の default
キーワードを正しく解釈し、対応するランタイム関数 sys.selectdefault
を呼び出すコードを生成するようになります。そして、Goランタイムはこの sys.selectdefault
関数を通じて、default
ケースが選択された際の適切な動作(ブロッキングせずに即座に実行)を保証します。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に src/cmd/gc/walk.c
と src/runtime/chan.c
に集中しています。
src/cmd/gc/walk.c
の変更
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -1246,6 +1246,8 @@ selcase(Node *n, Node *var)\n \tNode *a, *r, *on, *c;\n \tType *t;\n \n+\tif(n->left == N)\n+\t\tgoto dflt;\n \tc = n->left;\n \tif(c->op == ORECV)\n \t\tgoto recv;\n@@ -1329,6 +1331,14 @@ recv2:\n \tr = list(a, r);\n \ta = var;\t\t\t// sel-var\n \tr = list(a, r);\n+\tgoto out;\n+\n+dflt:\n+\t// selectdefault(sel *byte);\n+\ton = syslook(\"selectdefault\", 0);\n+\ta = var;\n+\tr = a;\t\t\t\t// sel-var\n+\tgoto out;\n \n out:\n \ta = nod(OCALL, on, r);\
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -1367,8 +1377,8 @@ walkselect(Node *sel)\n {\n \tIter iter;\n \tNode *n, *oc, *on, *r;\n-\tNode *var, *bod, *res;\n-\tint count;\n+\tNode *var, *bod, *res, *def;\n+\tint count, op;\n \tint32 lno;\n \n \tlno = setlineno(sel);\n@@ -1385,6 +1395,7 @@ walkselect(Node *sel)\n \tres = N;\t// entire select body\n \tbod = N;\t// body of each case\n \toc = N;\t\t// last case\n+\tdef = N;\t// default case\n \n \tfor(count=0; n!=N; n=listnext(&iter)) {\n \t\tsetlineno(n);\n@@ -1395,15 +1406,22 @@ walkselect(Node *sel)\n \t\t\tbreak;\n \n \t\tcase OXCASE:\n-\t\t\tswitch(n->left->op) {\n+\t\t\tif(n->left == N) {\n+\t\t\t\top = ORECV;\t// actual value not used\n+\t\t\t\tif(def != N)\n+\t\t\t\t\tyyerror(\"only one default select allowed\");\n+\t\t\t\tdef = n;\n+\t\t\t} else\n+\t\t\t\top = n->left->op;\n+\t\t\tswitch(op) {\n \t\t\tdefault:\n-\t\t\t\tyyerror(\"select cases must be send or recv\");\n+\t\t\t\t// select cases must be send, recv or default\n+\t\t\t\tyyerror(\"select cases must be send, recv or default\");\n \t\t\t\tbreak;\n \n \t\t\tcase OAS:\n \t\t\t\t// convert new syntax (a=recv(chan)) to (recv(a,chan))\n \t\t\t\tif(n->left->right == N || n->left->right->op != ORECV) {\n-\t\t\t\t\tyyerror(\"select cases must be send or recv\");\n+\t\t\t\t\tyyerror(\"select cases must be send, recv or default\");\n \t\t\t\t\tbreak;\n \t\t\t\t}\n \t\t\t\tn->left->right->right = n->left->right->left;\
src/runtime/chan.c
の変更
--- a/src/runtime/chan.c
+++ b/src/runtime/chan.c
@@ -497,14 +497,14 @@ sys·selectrecv(Select *sel, Hchan *c, ...)\n \t}\n }\n \n+\n+// selectrecv(sel *byte) (selected bool);\n void\n-sys·selectdefault(Select *sel)\n+sys·selectdefault(Select *sel, ...)\n {\n-\tint32 i, eo;\n+\tint32 i;\n \tScase *cas;\n-\tHchan *c;\n \t\n-\tc = nil;\n \ti = sel->ncase;\n \tif(i >= sel->tcase)\n \t\tthrow(\"selectdefault: too many cases\");\n@@ -512,13 +512,11 @@ sys·selectdefault(Select *sel)\n \tcas = &sel->scase[i];\n \n \tcas->pc = sys·getcallerpc(&sel);\n-\tcas->chan = c;\n+\tcas->chan = nil;\n \n-\teo = rnd(sizeof(sel), sizeof(c));\n-\teo = rnd(eo+sizeof(c), sizeof(byte*));\n-\tcas->so = rnd(eo+sizeof(byte*), 1);\n+\tcas->so = rnd(sizeof(sel), 1);\n \tcas->send = 2;\n-\tcas->u.elemp = *(byte**)((byte*)&sel + eo);\n+\tcas->u.elemp = nil;\n \n \tif(debug) {\n \t\tprints(\"newselect s=\");
コアとなるコードの解説
src/cmd/gc/walk.c
の解説
walk.c
はGoコンパイラのバックエンドの一部であり、Goのソースコードから生成された抽象構文木 (AST) を走査し、実行可能なバイナリコードを生成するための準備を行います。
-
selcase
関数の変更:if(n->left == N)
: これはselect
ステートメントのcase
節がdefault
キーワードであるかどうかをチェックする新しい条件です。GoのASTでは、default
ケースはleft
フィールドがN
(NULL) に設定されたOXCASE
ノードとして表現されます。goto dflt;
:default
ケースが検出された場合、新しいdflt
ラベルにジャンプします。dflt:
ラベル以下:on = syslook("selectdefault", 0);
:syslook
関数は、Goランタイムの内部関数であるselectdefault
のシンボルを検索します。このシンボルは、select
のdefault
ケースを処理するためのランタイム関数を指します。a = var; r = a;
:selectdefault
関数に渡す引数を準備しています。var
はselect
ステートメント全体の状態を管理する内部変数(Select
構造体へのポインタ)です。goto out;
: 引数準備後、共通のコード生成パスにジャンプします。 この変更により、コンパイラはdefault
ケースを認識し、それをsys.selectdefault
ランタイム関数への呼び出しに変換するコードを生成するようになります。
-
walkselect
関数の変更:Node *var, *bod, *res, *def;
:def
という新しいNode
ポインタが追加されました。これは、select
ステートメント内にdefault
ケースが存在する場合、そのdefault
ケースのASTノードを保持するために使用されます。def = N;
:def
は初期化時にN
(NULL) に設定されます。case OXCASE:
ブロック内:if(n->left == N)
: ここでもdefault
ケースの検出が行われます。if(def != N) yyerror("only one default select allowed");
: Go言語の仕様ではselect
ステートメントにdefault
ケースは1つしか許されないため、既にdefault
ケースが検出されている場合はコンパイルエラー (yyerror
) を発生させます。def = n;
: 最初のdefault
ケースが検出された場合、そのノードをdef
に保存します。- エラーメッセージの変更:
yyerror
のメッセージがselect cases must be send or recv
からselect cases must be send, recv or default
に更新され、default
が正当なselect
ケースであることを明示しています。 これらの変更により、コンパイラはdefault
ケースの存在を追跡し、Go言語のセマンティクス(default
ケースは1つのみ)を強制するようになります。
src/runtime/chan.c
の解説
chan.c
はGoランタイムの一部であり、チャネルの作成、送受信、そして select
ステートメントの実行といった低レベルなチャネル操作を扱います。
sys·selectdefault
関数の実装:void sys·selectdefault(Select *sel, ...)
: この関数は、コンパイラによって生成されたコードから呼び出されるランタイム関数です。sel
はselect
操作の状態を管理するSelect
構造体へのポインタです。Scase *cas;
:Scase
はselect
ステートメント内の個々のケース(チャネル送受信またはdefault
)を表す構造体です。cas = &sel->scase[i];
:select
構造体内のscase
配列の次の空きスロットに、このdefault
ケースの情報を格納します。cas->pc = sys·getcallerpc(&sel);
: このdefault
ケースが実行された際の呼び出し元のプログラムカウンタ (PC) を記録します。デバッグやスタックトレースに利用されます。cas->chan = nil;
:default
ケースは特定のチャネルに関連しないため、chan
フィールドはnil
に設定されます。cas->send = 2;
:send
フィールドはケースの種類を示します。2
はdefault
ケースを表す新しい値です(0
は受信、1
は送信)。cas->u.elemp = nil;
:default
ケースは値の送受信を伴わないため、要素ポインタもnil
に設定されます。cas->so = rnd(sizeof(sel), 1);
:so
(offset) は、このケースに関連するデータがスタック上のどこにあるかを示すオフセットです。default
ケースでは、select
構造体自体のサイズに基づいて計算されます。 このランタイム関数の追加により、select
ステートメントがdefault
ケースを持つ場合に、どのチャネル操作も準備ができていないときに、ブロッキングせずにdefault
ケースのコードブロックが実行されるメカニズムが確立されます。
これらの変更は、Go言語の select
ステートメントが、ノンブロッキングな動作を可能にする default
ケースをサポートするための、コンパイラとランタイムにわたる協調的な実装を示しています。
関連リンク
- Go言語の
select
ステートメントに関する公式ドキュメント: https://go.dev/tour/concurrency/5 - Go言語のチャネルに関する公式ドキュメント: https://go.dev/tour/concurrency/2
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/gc/
とsrc/runtime/
ディレクトリ) - Go言語のコンパイラとランタイムの内部構造に関する一般的な知識
- CSP (Communicating Sequential Processes) の概念