[インデックス 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のセマンティクス(ok
idiom)とは異なります。この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
は発生せず、ok
idiom が導入されます。
-
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 の基本的な概念