[インデックス 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) の概念