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

[インデックス 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 ステートメントの基本的な動作は以下の通りです。

  1. select 内のすべての case 式(チャネルの送受信操作)が評価されます。
  2. 準備ができている(ブロッキングせずに実行できる)case が複数ある場合、その中からランダムに1つが選択されて実行されます。
  3. 準備ができている case が1つだけの場合、その case が実行されます。
  4. default ケースがない場合: どの case も準備ができていない場合、select ステートメント全体がブロックされ、いずれかの case が準備できるまで待機します。
  5. default ケースがある場合: どの case も準備ができていない場合、default ケースがすぐに実行されます。これにより、select ステートメントはブロッキングせずに続行できます。

default ケースは、ノンブロッキングなチャネル操作を実現したり、タイムアウト処理を実装したりする際に非常に有用です。

select {
case <-ch1:
    // ch1 から値を受信
case ch2 <- value:
    // ch2 へ値を送信
default:
    // どのチャネル操作も準備ができていない場合に実行
}

技術的詳細

このコミットは、Goコンパイラ (gc) とランタイム (runtime) の両方にわたる変更を含んでおり、select ステートメントにおける default ケースのサポートを統合的に実現しています。

コンパイラ (src/cmd/gc/) 側の変更

  1. シンボル認識 (src/cmd/gc/subr.c): isselect 関数は、特定のシンボルが select ステートメントに関連するかどうかを判断します。このコミットでは、selectdefault という新しいシンボルが追加され、コンパイラが default ケースを認識できるようにしています。これは、selectdefault ケースが内部的に sys.selectdefault というランタイム関数呼び出しに変換されるためです。

  2. システム関数のエクスポート (src/cmd/gc/sys.go): sys.go ファイルは、GoコンパイラがGoプログラムから呼び出すことができる内部的なシステム関数を定義しています。export func selectdefault(sel *byte) (selected bool); の行が追加され、selectdefault ランタイム関数がコンパイラから利用可能になります。

  3. システム関数のインポート定義 (src/cmd/gc/sysimport.c): sysimport.c は、コンパイラがGoのソースコードを解析する際に、Goランタイムの内部関数をどのように認識するかを定義するC言語の文字列を含んでいます。export func sys.selectdefault (sel *uint8) (selected bool)\n が追加され、selectdefault 関数がGoの組み込み関数として扱われるようになります。

  4. 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/) 側の変更

  1. 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 ケースの特性に合わせて調整されています。
  2. rnd 関数の調整 (src/runtime/runtime.c, src/runtime/runtime.h):

    • src/runtime/runtime.cint32 maxround = 8; というグローバル変数が追加されました。
    • rnd 関数は、アライメントを考慮してサイズを丸めるためのユーティリティ関数です。このコミットでは、rnd 関数内で m > maxround の場合に m = maxround とすることで、丸め処理の最大値を制限しています。これは、selectdefault の引数処理におけるアライメント計算の安定性や効率性に関連している可能性があります。
    • src/runtime/runtime.hextern int32 maxround; が追加され、maxround 変数が他のランタイムファイルから参照可能になっています。

これらの変更により、Goコンパイラは select ステートメント内の default キーワードを正しく解釈し、対応するランタイム関数 sys.selectdefault を呼び出すコードを生成するようになります。そして、Goランタイムはこの sys.selectdefault 関数を通じて、default ケースが選択された際の適切な動作(ブロッキングせずに即座に実行)を保証します。

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

このコミットのコアとなる変更は、主に src/cmd/gc/walk.csrc/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) を走査し、実行可能なバイナリコードを生成するための準備を行います。

  1. 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 のシンボルを検索します。このシンボルは、selectdefault ケースを処理するためのランタイム関数を指します。
      • a = var; r = a;: selectdefault 関数に渡す引数を準備しています。varselect ステートメント全体の状態を管理する内部変数(Select 構造体へのポインタ)です。
      • goto out;: 引数準備後、共通のコード生成パスにジャンプします。 この変更により、コンパイラは default ケースを認識し、それを sys.selectdefault ランタイム関数への呼び出しに変換するコードを生成するようになります。
  2. 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 ステートメントの実行といった低レベルなチャネル操作を扱います。

  1. sys·selectdefault 関数の実装:
    • void sys·selectdefault(Select *sel, ...): この関数は、コンパイラによって生成されたコードから呼び出されるランタイム関数です。selselect 操作の状態を管理する Select 構造体へのポインタです。
    • Scase *cas;: Scaseselect ステートメント内の個々のケース(チャネル送受信または 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 フィールドはケースの種類を示します。2default ケースを表す新しい値です(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言語のソースコード (特に src/cmd/gc/src/runtime/ ディレクトリ)
  • Go言語のコンパイラとランタイムの内部構造に関する一般的な知識
  • CSP (Communicating Sequential Processes) の概念