[インデックス 1820] ファイルの概要
このコミットは、Go言語のチャネルに close および closed (後の ok idiom の一部となる機能) の概念を導入するものです。Goコンパイラ (src/cmd/gc) とGoランタイム (src/runtime) の両方にわたる広範な変更が含まれています。
変更された主なファイルとその役割は以下の通りです。
src/cmd/gc/Makefile: ビルドスクリプトの修正。mkbuiltinスクリプトの実行パスが変更されています。src/cmd/gc/builtin.c.boot: コンパイラが認識する組み込み関数のブートストラップファイル。sys.closechanとsys.closedchanの宣言が追加されています。src/cmd/gc/go.h: コンパイラ内部で使用されるオペレーションコード (opcode) の定義ファイル。OCLOSEとOCLOSEDという新しいopcodeが追加されています。src/cmd/gc/go.y: Go言語の構文解析器 (Yacc/Bisonの文法定義ファイル)。closeとclosedがキーワードとして認識され、対応する構文規則が追加されています。src/cmd/gc/lex.c: Go言語の字句解析器 (lexer)。closeとclosedが新しいキーワードとして登録されています。src/cmd/gc/mkbuiltin: 組み込み関数を生成するスクリプト。p4 openコマンドのパスが修正されています。src/cmd/gc/subr.c: コンパイラ内部のユーティリティ関数やデータ構造を定義するファイル。opnames配列にOCLOSEとOCLOSEDの文字列表現が追加され、デバッグ時の可読性が向上しています。src/cmd/gc/sys.go: Goコンパイラがランタイム関数を呼び出すための内部的な宣言ファイル。closechanとclosedchanの関数シグネチャが追加されています。src/cmd/gc/walk.c: コンパイラの「ウォーク」フェーズを担当するファイル。抽象構文木 (AST) を走査し、型チェックや最適化、ランタイム関数への変換などを行います。OCLOSEとOCLOSEDオペレーションをsys.closechanおよびsys.closedchanランタイム関数呼び出しに変換するロジックが追加されています。src/runtime/chan.c: Goランタイムにおけるチャネルの実装ファイル。closeおよびclosedの実際の動作を定義するsys.closechanとsys.closedchan関数が追加され、チャネルの状態を管理するための新しいフラグが導入されています。
コミット
chan flags close/closed installed
runtime not finished.
R=r
OCL=26217
CL=26217
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6eb54cb05bc3ed52aac3990da9d7bb372ae7cbab
元コミット内容
チャネルの close および closed フラグが導入されました。ランタイムの実装はまだ完了していません。
変更の背景
Go言語の並行処理モデルにおいて、チャネルはゴルーチン間の安全な通信を可能にする重要なプリミティブです。しかし、チャネルを通じてデータの送受信を行うだけでなく、そのチャネルが「閉じられた」状態にあるかどうかを判断したり、明示的にチャネルを閉じてそれ以上データが送信されないことを通知したりするメカニズムが必要とされていました。
このコミット以前は、チャネルが閉じられたことを検出する標準的な方法や、チャネルを閉じるための組み込み関数が存在しませんでした。これにより、チャネルを介した通信の終了を適切に管理することが困難であり、特に複数の送信者が存在するシナリオや、受信側がチャネルの終了を待機する必要があるシナリオで問題が生じていました。
close および closed (後の ok idiom の一部) の導入は、これらの課題を解決し、Goのチャネルベースの並行処理をより堅牢で表現力豊かなものにするための基礎を築くものです。close は送信側がチャネルの終了を通知するために使用され、closed は受信側がチャネルが閉じられたことを検出するために使用されます。これにより、ゴルーチン間の協調的なシャットダウンや、チャネルからのデータストリームの終了処理がより容易になります。
コミットメッセージにある「runtime not finished.」という記述は、この時点では基本的な機能が導入されたものの、チャネルのクローズに関するすべてのエッジケース(例:閉じられたチャネルへの送信、既に閉じられたチャネルのクローズ)に対する完全なエラーハンドリングやセマンティクスがまだ洗練されていない初期段階であることを示唆しています。
前提知識の解説
このコミットの理解には、以下のGo言語およびコンパイラの基本的な概念が役立ちます。
-
Go言語のチャネル:
- チャネルは、Goのゴルーチン間で値を送受信するための通信メカニズムです。
make(chan Type)で作成され、ch <- valueで送信、value := <-chで受信します。 - バッファなしチャネル(同期チャネル)とバッファありチャネル(非同期チャネル)があります。
- チャネルは、並行処理における同期と通信の強力なツールです。
- チャネルは、Goのゴルーチン間で値を送受信するための通信メカニズムです。
-
Goコンパイラ (
gc) の構造: Goコンパイラ (gc) は、Goのソースコードを実行可能なバイナリに変換するプロセスを複数のフェーズに分けて行います。- 字句解析 (Lexing): ソースコードをトークン(キーワード、識別子、演算子など)のストリームに変換します。
src/cmd/gc/lex.cがこの役割を担います。 - 構文解析 (Parsing): トークンのストリームを解析し、プログラムの構造を表す抽象構文木 (AST) を構築します。
src/cmd/gc/go.y(Yacc/Bisonの文法定義) がこの役割を担います。 - AST (Abstract Syntax Tree): ソースコードの構造を木構造で表現したものです。コンパイラの各フェーズはこのASTを操作します。
- ウォークフェーズ (Walking): ASTを走査し、型チェック、定数畳み込み、組み込み関数の変換、ランタイム関数呼び出しへの変換など、様々な最適化や変換を行います。
src/cmd/gc/walk.cがこのフェーズの主要な部分です。 - オペレーションコード (Opcode): コンパイラ内部でASTノードの種類を表す定数です。例えば、
OASは代入、OCALLは関数呼び出しを表します。src/cmd/gc/go.hで定義されます。 - 組み込み関数 (Built-in Functions):
len,cap,new,makeなど、Go言語に最初から用意されている特殊な関数です。これらは通常の関数呼び出しとは異なり、コンパイラによって特別に処理されます。src/cmd/gc/builtin.c.bootやsrc/cmd/gc/sys.goでその宣言や内部的なマッピングが行われます。
- 字句解析 (Lexing): ソースコードをトークン(キーワード、識別子、演算子など)のストリームに変換します。
-
Goランタイム (
runtime):- Goプログラムの実行をサポートする低レベルのコード群です。ゴルーチンのスケジューリング、ガベージコレクション、チャネル操作、メモリ管理など、Go言語の並行処理とメモリモデルの基盤を提供します。
- コンパイラによって生成されたコードは、必要に応じてランタイムの関数を呼び出します。チャネル操作 (
send,recv,close) の実際のロジックはsrc/runtime/chan.cに実装されています。
このコミットは、close と closed という新しい概念をGo言語に導入するために、字句解析器、構文解析器、ASTの定義、ウォークフェーズ、そして最終的なランタイム実装という、コンパイラとランタイムの複数の層にわたる協調的な変更を必要とします。
技術的詳細
このコミットは、Go言語に close および closed (チャネルが閉じられたかどうかをチェックする機能) を導入するために、コンパイラとランタイムの両方にわたる一連の変更を加えています。
-
字句解析器と構文解析器の更新:
src/cmd/gc/lex.cにcloseとclosedが新しいキーワードLCLOSEとLCLOSEDとして追加されました。これにより、コンパイラはこれらの単語を特別な意味を持つトークンとして認識できるようになります。src/cmd/gc/go.yには、これらの新しいキーワードに対応する構文規則が追加されました。具体的には、close(expr)とclosed(expr)の形式がpexpr(プライマリ式) の一部として認識されるようになります。これにより、Goのコード内でclose(ch)やclosed(ch)のような記述が可能になります。構文解析器はこれらの式を、それぞれOCLOSEおよびOCLOSEDという内部的なオペレーションコードを持つASTノードに変換します。
-
コンパイラ内部オペレーションコードの導入:
src/cmd/gc/go.hにOCLOSEとOCLOSEDという新しいenum値が追加されました。これらは、コンパイラがチャネルのクローズ操作とクローズ状態チェック操作をAST上で表現するための内部的な識別子です。src/cmd/gc/subr.cのopnames配列にもこれらの新しいオペレーションコードに対応する文字列が追加され、デバッグやログ出力時にこれらの操作が識別しやすくなっています。
-
ウォークフェーズでの変換 (
src/cmd/gc/walk.c):- コンパイラの
walkフェーズは、ASTを走査し、高レベルのGoの構文を低レベルのランタイム関数呼び出しに変換する役割を担います。 src/cmd/gc/walk.cのwalk関数内のswitchステートメントにOCLOSEとOCLOSEDのケースが追加されました。OCLOSEの場合、walktype(n->left, Erv)を呼び出してチャネルの式を評価し、chanop(n, top)を呼び出してsys.closechanランタイム関数への呼び出しに変換します。OCLOSEDの場合も同様に、walktype(n->left, Erv)でチャネルの式を評価し、chanop(n, top)を呼び出してsys.closedchanランタイム関数への呼び出しに変換します。chanop関数は、与えられたチャネルの型に基づいて、適切なランタイム関数 (sys.closechanまたはsys.closedchan) を選択し、その関数呼び出しを表すASTノードを構築します。この変換により、Goのソースコードで書かれたclose(ch)やclosed(ch)は、最終的にGoランタイムのCコードで実装されたsys.closechanやsys.closedchan関数への呼び出しに置き換えられます。
- コンパイラの
-
ランタイム関数の宣言 (
src/cmd/gc/sys.go):src/cmd/gc/sys.goは、Goコンパイラがランタイム関数を呼び出すために使用する内部的なGoの関数宣言を含んでいます。func closechan(hchan chan any)とfunc closedchan(hchan chan any) boolの宣言が追加されました。これらは、コンパイラが生成するコードがランタイムのC関数sys·closechanおよびsys·closedchanを呼び出すためのGo側のインターフェースを提供します。
-
ランタイム実装 (
src/runtime/chan.c):- Goランタイムのチャネル実装ファイルである
src/runtime/chan.cに、sys·closechanとsys·closedchanの実際のC言語実装が追加されました。 Hchan構造体の変更:Hchan構造体(チャネルの内部表現)にuint16 closed;フィールドが追加されました。このフィールドはチャネルのクローズ状態を管理するためのフラグを保持します。- 新しいフラグの定義:
Wclosed = 0x0001:close()がチャネルに対して呼び出されたことを示すフラグ。Rclosed = 0xfffe:closed()が呼び出された後に、チャネルが閉じられたことを示すフラグ(読み取りカウント)。Rincr = 0x0002:Rclosedフラグをインクリメントするための値。Rmax = 0x8000:Rclosedの最大値。これを超えるとclosedchanがthrowする(後のバージョンで変更される可能性が高い、初期の実装)。
sys·closechan(Hchan *c):- チャネル
cがnilでないことを確認します。 c->closedフィールドにWclosedフラグをセットします。これにより、チャネルが閉じられた状態であることをマークします。- 既に
Wclosedがセットされている場合は何もしません(冪等性)。
- チャネル
sys·closedchan(Hchan *c, bool closed):- チャネル
cがnilでないことを確認します。 closed変数を初期化します。c->closedにRclosedフラグがセットされているかを確認します。- もし
Rclosedがセットされており、かつRmaxに達していない場合、c->closedをRincrだけインクリメントし、closedを1に設定します。 Rmaxに達している場合はthrow("closedchan: ignored")となります。これは、closedが何度も呼び出された場合の初期的なエラーハンドリングであり、後のGoのセマンティクス(okidiom)とは異なります。このthrowは、この機能がまだ「runtime not finished」であることの具体的な証拠です。
- チャネル
- Goランタイムのチャネル実装ファイルである
これらの変更により、Go言語はチャネルのクローズとクローズ状態のチェックという、並行処理における重要な制御フローをサポートするようになりました。
コアとなるコードの変更箇所
src/cmd/gc/walk.c (チャネル操作の変換ロジック)
diff --git a/src/cmd/gc/walk.c b/src/cmd/gc/walk.c
index b8821e6f70..c113858d78 100644
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -126,6 +126,8 @@ loop:
case OASOP:
case OAS:
+ case OCLOSE:
+ case OCLOSED:
case OCALLMETH:
case OCALLINTER:
case OCALL:
@@ -852,6 +854,20 @@ loop:
}\n \t\tgoto ret;\n
+\tcase OCLOSE:\n+\t\tif(top != Etop)\n+\t\t\tgoto nottop;\n+\t\twalktype(n->left, Erv);\t\t// chan\n+\t\tindir(n, chanop(n, top));\n+\t\tgoto ret;\n+\n+\tcase OCLOSED:\n+\t\tif(top == Elv)\n+\t\t\tgoto nottop;\n+\t\twalktype(n->left, Erv);\t\t// chan\n+\t\tindir(n, chanop(n, top));\n+\t\tgoto ret;\n+\
\tcase OSEND:\n \t\tif(top == Elv)\n \t\t\tgoto nottop;\
@@ -2447,6 +2463,40 @@ chanop(Node *n, int top)\n \tdefault:\n \t\tfatal(\"chanop: unknown op %O\", n->op);\n \n+\tcase OCLOSE:\n+\t\t// closechan(hchan *chan any);\n+\t\tt = fixchan(n->left->type);\n+\t\tif(t == T)\n+\t\t\tbreak;\n+\n+\t\ta = n->left;\t\t\t// chan\n+\t\tr = a;\n+\n+\t\ton = syslook(\"closechan\", 1);\n+\t\targtype(on, t->type);\t// any-1\n+\n+\t\tr = nod(OCALL, on, r);\n+\t\twalktype(r, top);\n+\t\tr->type = n->type;\n+\t\tbreak;\n+\n+\tcase OCLOSED:\n+\t\t// closedchan(hchan *chan any) bool;\n+\t\tt = fixchan(n->left->type);\n+\t\tif(t == T)\n+\t\t\tbreak;\n+\n+\t\ta = n->left;\t\t\t// chan\n+\t\tr = a;\n+\n+\t\ton = syslook(\"closedchan\", 1);\n+\t\targtype(on, t->type);\t// any-1\n+\n+\t\tr = nod(OCALL, on, r);\n+\t\twalktype(r, top);\n+\t\tn->type = r->type;\n+\t\tbreak;\n+\n \tcase OMAKE:\n \t\tcl = listcount(n->left);\n \t\tif(cl > 1)\n```
### `src/runtime/chan.c` (ランタイムでのチャネル実装)
```c
diff --git a/src/runtime/chan.c b/src/runtime/chan.c
index 7e6f830f6c..a15e50dc02 100644
--- a/src/runtime/chan.c
+++ b/src/runtime/chan.c
@@ -7,6 +7,14 @@\n static\tint32\tdebug\t= 0;\n static\tLock\t\tchanlock;\n \n+enum\n+{\n+\tWclosed\t\t= 0x0001,\n+\tRclosed\t\t= 0xfffe,\n+\tRincr\t\t= 0x0002,\n+\tRmax\t\t= 0x8000,\n+};\n+\
typedef\tstruct\tHchan\tHchan;\n typedef\tstruct\tLink\tLink;\n typedef\tstruct\tWaitQ\tWaitQ;\
@@ -32,7 +40,9 @@ struct\tWaitQ\n \n struct\tHchan\n {\n-\tuint32\telemsize;\n+\tuint16\telemsize;\n+\tuint16\tclosed;\t\t\t// Wclosed closed() hash been called\n+\t\t\t\t\t// Rclosed read-count after closed()\n \tuint32\tdataqsiz;\t\t// size of the circular q\n \tuint32\tqcount;\t\t\t// total data in the q\n \tAlg*\telemalg;\t\t// interface for element type\
@@ -535,7 +545,6 @@ sys·selectdefault(Select *sel, ...)\n \t}\n }\n \n-\n // selectgo(sel *byte);\n void\n sys·selectgo(Select *sel)\
@@ -790,6 +799,42 @@ retc:\n \t*as = true;\n }\n \n+// closechan(sel *byte);\n+void\n+sys·closechan(Hchan *c)\n+{\n+\tif(c == nil)\n+\t\tthrow(\"closechan: channel not allocated\");\n+\n+\t// if wclosed already set\n+\t// work has been done - just return\n+\tif(c->closed & Wclosed)\n+\t\treturn;\n+\n+\t// set wclosed\n+\tc->closed |= Wclosed;\n+}\n+\n+// closedchan(sel *byte) bool;\n+void\n+sys·closedchan(Hchan *c, bool closed)\n+{\n+\tif(c == nil)\n+\t\tthrow(\"closedchan: channel not allocated\");\n+\n+\tclosed = 0;\n+\n+\t// test rclosed\n+\tif(c->closed & Rclosed) {\n+\t\t// see if rclosed has been set a lot\n+\t\tif(c->closed & Rmax)\n+\t\t\tthrow(\"closedchan: ignored\");\n+\t\tc->closed += Rincr;\n+\t\tclosed = 1;\n+\t}\n+\tFLUSH(&closed);\n+}\n+\
static SudoG*\n dequeue(WaitQ *q, Hchan *c)\n {\
コアとなるコードの解説
src/cmd/gc/walk.c の解説
src/cmd/gc/walk.c は、Goコンパイラの重要なフェーズである「ウォーク」を担当します。このフェーズでは、構文解析器によって生成された抽象構文木 (AST) を走査し、Go言語のセマンティクスを低レベルのランタイム関数呼び出しに変換します。
-
case OCLOSE:とcase OCLOSED:の追加:walk関数内の大きなswitchステートメントに、新しく定義されたOCLOSEとOCLOSEDオペレーションコードのケースが追加されています。これは、コンパイラがclose(ch)やclosed(ch)のようなGoのコードをAST上で見つけたときに、これらのケースに分岐して処理を行うことを意味します。if(top != Etop)やif(top == Elv)のチェックは、式のコンテキスト(例:ステートメントとして使われているか、値として使われているか)を検証しています。closeはステートメントとしてのみ有効であり、closedはブール値を返すため、そのコンテキストが適切であるかを確認しています。walktype(n->left, Erv):n->leftはcloseまたはclosedの引数であるチャネルの式を表します。この行は、そのチャネル式を評価し、その型情報を取得します。Ervは「右辺値」のコンテキストを示します。indir(n, chanop(n, top)): ここが変換の核心です。chanop関数が呼び出され、OCLOSEまたはOCLOSEDオペレーションを、対応するランタイム関数 (sys.closechanまたはsys.closedchan) への呼び出しに変換します。indirは、その結果を現在のASTノードnに適用します。
-
chanop関数のcase OCLOSE:とcase OCLOSED:の追加:chanop関数は、チャネルに関連するASTオペレーション(送受信、クローズなど)を、ランタイム関数呼び出しに変換する具体的なロジックを含んでいます。t = fixchan(n->left->type): チャネルの型情報を取得し、正規化します。a = n->left; r = a;: チャネルのASTノードをaとrに代入します。on = syslook("closechan", 1);またはon = syslook("closedchan", 1);:syslook関数は、指定された名前(例:"closechan")を持つランタイム関数をコンパイラのシンボルテーブルから検索し、そのシンボルへのポインタを返します。1は、その関数が組み込み関数であることを示します。argtype(on, t->type);: ランタイム関数の引数として、チャネルの型を設定します。r = nod(OCALL, on, r);:OCALLオペレーションコードを持つ新しいASTノードを作成します。このノードは、on(ランタイム関数) をr(チャネル) を引数として呼び出すことを表します。walktype(r, top);: 新しく作成されたランタイム関数呼び出しのASTノードを再度ウォークし、最終的なコード生成の準備をします。r->type = n->type;またはn->type = r->type;: 変換後のノードの型を元のノードの型に合わせます。closedの場合はブール値を返すため、その型が設定されます。
これらの変更により、Goのソースコードで close(ch) と書かれた部分は、コンパイル時にGoランタイムの sys·closechan 関数への呼び出しに、closed(ch) は sys·closedchan 関数への呼び出しに変換されるようになります。
src/runtime/chan.c の解説
src/runtime/chan.c は、Goランタイムにおけるチャネルの低レベルな実装を含んでいます。このファイルへの変更は、close および closed の実際の動作を定義します。
-
enumの追加: チャネルのクローズ状態を管理するための新しいフラグが定義されています。Wclosed = 0x0001: 「Write closed」の略で、close()関数がこのチャネルに対して呼び出されたことを示します。Rclosed = 0xfffe: 「Read closed」の略で、チャネルが閉じられた後にclosed()が呼び出された回数に関連するフラグです。この値は、後のGoのセマンティクスとは異なり、初期の実装における試行錯誤の痕跡が見られます。Rincr = 0x0002:Rclosedフラグをインクリメントするための値。Rmax = 0x8000:Rclosedの最大値。この値を超えると、closedchanがエラーをスローするようになっています。これは、この機能がまだ「runtime not finished」であることの具体的な証拠であり、後のGoのバージョンではこのようなthrowは発生せず、okidiom が導入されます。
-
Hchan構造体の変更: チャネルの内部表現であるHchan構造体にuint16 closed;フィールドが追加されました。このフィールドは、上記のフラグを組み合わせてチャネルのクローズ状態を保持します。elemsizeがuint32からuint16に変更され、その分closedフィールドが追加されたようです。 -
sys·closechan(Hchan *c)関数の追加: この関数は、Goのclose(ch)ステートメントに対応するランタイム実装です。if(c == nil) throw("closechan: channel not allocated");:nilチャネルをクローズしようとした場合のエラーチェックです。if(c->closed & Wclosed) return;: 既にチャネルが閉じられている(Wclosedフラグがセットされている)場合は、何もしません。これにより、close操作の冪等性が保証されます。c->closed |= Wclosed;: チャネルが閉じられたことを示すWclosedフラグをHchan構造体のclosedフィールドにセットします。
-
sys·closedchan(Hchan *c, bool closed)関数の追加: この関数は、Goのclosed(ch)(後のv, ok := <-chのok部分) に対応するランタイム実装です。if(c == nil) throw("closedchan: channel not allocated");:nilチャネルのクローズ状態をチェックしようとした場合のエラーチェックです。closed = 0;: 戻り値となるclosed変数を初期化します。if(c->closed & Rclosed) { ... }:Rclosedフラグがセットされているかを確認します。これは、チャネルが閉じられた後に読み取りが行われたことを示す初期的な試みです。if(c->closed & Rmax) throw("closedchan: ignored");:Rmaxに達している場合、エラーをスローします。これは、closedが何度も呼び出された場合の初期的なエラーハンドリングであり、後のGoのセマンティクスとは大きく異なります。c->closed += Rincr;:Rclosedフラグをインクリメントします。closed = 1;:closed変数をtrueに設定します。
FLUSH(&closed);:closed変数の値をメモリにフラッシュします。
この sys·closedchan の実装は、コミットメッセージの「runtime not finished.」という記述を裏付けるものです。特に Rclosed や Rmax の扱いは、後のGoのチャネルの ok idiom (v, ok := <-ch) のセマンティクスとは異なり、初期の実験的な実装であることが伺えます。最終的には、チャネルが閉じられた後に値を受信しようとすると、その型のゼロ値と false が返されるという、より洗練されたメカニズムに進化します。
関連リンク
- Go言語のチャネルに関する公式ドキュメント (現在のバージョン):
参考にした情報源リンク
- Go言語のソースコード (golang/go GitHubリポジトリ)
- Go言語のコンパイラとランタイムに関する一般的な知識
- Yacc/Bison および Lex/Flex の基本的な概念