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

[インデックス 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.closechansys.closedchan の宣言が追加されています。
  • src/cmd/gc/go.h: コンパイラ内部で使用されるオペレーションコード (opcode) の定義ファイル。OCLOSEOCLOSED という新しいopcodeが追加されています。
  • src/cmd/gc/go.y: Go言語の構文解析器 (Yacc/Bisonの文法定義ファイル)。closeclosed がキーワードとして認識され、対応する構文規則が追加されています。
  • src/cmd/gc/lex.c: Go言語の字句解析器 (lexer)。closeclosed が新しいキーワードとして登録されています。
  • src/cmd/gc/mkbuiltin: 組み込み関数を生成するスクリプト。p4 open コマンドのパスが修正されています。
  • src/cmd/gc/subr.c: コンパイラ内部のユーティリティ関数やデータ構造を定義するファイル。opnames 配列に OCLOSEOCLOSED の文字列表現が追加され、デバッグ時の可読性が向上しています。
  • src/cmd/gc/sys.go: Goコンパイラがランタイム関数を呼び出すための内部的な宣言ファイル。closechanclosedchan の関数シグネチャが追加されています。
  • src/cmd/gc/walk.c: コンパイラの「ウォーク」フェーズを担当するファイル。抽象構文木 (AST) を走査し、型チェックや最適化、ランタイム関数への変換などを行います。OCLOSEOCLOSED オペレーションを sys.closechan および sys.closedchan ランタイム関数呼び出しに変換するロジックが追加されています。
  • src/runtime/chan.c: Goランタイムにおけるチャネルの実装ファイル。close および closed の実際の動作を定義する sys.closechansys.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言語およびコンパイラの基本的な概念が役立ちます。

  1. Go言語のチャネル:

    • チャネルは、Goのゴルーチン間で値を送受信するための通信メカニズムです。make(chan Type) で作成され、ch <- value で送信、value := <-ch で受信します。
    • バッファなしチャネル(同期チャネル)とバッファありチャネル(非同期チャネル)があります。
    • チャネルは、並行処理における同期と通信の強力なツールです。
  2. 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.bootsrc/cmd/gc/sys.go でその宣言や内部的なマッピングが行われます。
  3. Goランタイム (runtime):

    • Goプログラムの実行をサポートする低レベルのコード群です。ゴルーチンのスケジューリング、ガベージコレクション、チャネル操作、メモリ管理など、Go言語の並行処理とメモリモデルの基盤を提供します。
    • コンパイラによって生成されたコードは、必要に応じてランタイムの関数を呼び出します。チャネル操作 (send, recv, close) の実際のロジックは src/runtime/chan.c に実装されています。

このコミットは、closeclosed という新しい概念をGo言語に導入するために、字句解析器、構文解析器、ASTの定義、ウォークフェーズ、そして最終的なランタイム実装という、コンパイラとランタイムの複数の層にわたる協調的な変更を必要とします。

技術的詳細

このコミットは、Go言語に close および closed (チャネルが閉じられたかどうかをチェックする機能) を導入するために、コンパイラとランタイムの両方にわたる一連の変更を加えています。

  1. 字句解析器と構文解析器の更新:

    • src/cmd/gc/lex.ccloseclosed が新しいキーワード LCLOSELCLOSED として追加されました。これにより、コンパイラはこれらの単語を特別な意味を持つトークンとして認識できるようになります。
    • src/cmd/gc/go.y には、これらの新しいキーワードに対応する構文規則が追加されました。具体的には、close(expr)closed(expr) の形式が pexpr (プライマリ式) の一部として認識されるようになります。これにより、Goのコード内で close(ch)closed(ch) のような記述が可能になります。構文解析器はこれらの式を、それぞれ OCLOSE および OCLOSED という内部的なオペレーションコードを持つASTノードに変換します。
  2. コンパイラ内部オペレーションコードの導入:

    • src/cmd/gc/go.hOCLOSEOCLOSED という新しい enum 値が追加されました。これらは、コンパイラがチャネルのクローズ操作とクローズ状態チェック操作をAST上で表現するための内部的な識別子です。
    • src/cmd/gc/subr.copnames 配列にもこれらの新しいオペレーションコードに対応する文字列が追加され、デバッグやログ出力時にこれらの操作が識別しやすくなっています。
  3. ウォークフェーズでの変換 (src/cmd/gc/walk.c):

    • コンパイラの walk フェーズは、ASTを走査し、高レベルのGoの構文を低レベルのランタイム関数呼び出しに変換する役割を担います。
    • src/cmd/gc/walk.cwalk 関数内の switch ステートメントに OCLOSEOCLOSED のケースが追加されました。
    • 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.closechansys.closedchan 関数への呼び出しに置き換えられます。
  4. ランタイム関数の宣言 (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側のインターフェースを提供します。
  5. ランタイム実装 (src/runtime/chan.c):

    • Goランタイムのチャネル実装ファイルである src/runtime/chan.c に、sys·closechansys·closedchan の実際のC言語実装が追加されました。
    • Hchan 構造体の変更: Hchan 構造体(チャネルの内部表現)に uint16 closed; フィールドが追加されました。このフィールドはチャネルのクローズ状態を管理するためのフラグを保持します。
    • 新しいフラグの定義:
      • Wclosed = 0x0001: close() がチャネルに対して呼び出されたことを示すフラグ。
      • Rclosed = 0xfffe: closed() が呼び出された後に、チャネルが閉じられたことを示すフラグ(読み取りカウント)。
      • Rincr = 0x0002: Rclosed フラグをインクリメントするための値。
      • Rmax = 0x8000: Rclosed の最大値。これを超えると closedchanthrow する(後のバージョンで変更される可能性が高い、初期の実装)。
    • sys·closechan(Hchan *c):
      • チャネル cnil でないことを確認します。
      • c->closed フィールドに Wclosed フラグをセットします。これにより、チャネルが閉じられた状態であることをマークします。
      • 既に Wclosed がセットされている場合は何もしません(冪等性)。
    • sys·closedchan(Hchan *c, bool closed):
      • チャネル cnil でないことを確認します。
      • closed 変数を初期化します。
      • c->closedRclosed フラグがセットされているかを確認します。
      • もし Rclosed がセットされており、かつ Rmax に達していない場合、c->closedRincr だけインクリメントし、closed1 に設定します。
      • Rmax に達している場合は throw("closedchan: ignored") となります。これは、closed が何度も呼び出された場合の初期的なエラーハンドリングであり、後のGoのセマンティクス(ok idiom)とは異なります。この throw は、この機能がまだ「runtime not finished」であることの具体的な証拠です。

これらの変更により、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 ステートメントに、新しく定義された OCLOSEOCLOSED オペレーションコードのケースが追加されています。これは、コンパイラが close(ch)closed(ch) のようなGoのコードをAST上で見つけたときに、これらのケースに分岐して処理を行うことを意味します。

    • if(top != Etop)if(top == Elv) のチェックは、式のコンテキスト(例:ステートメントとして使われているか、値として使われているか)を検証しています。close はステートメントとしてのみ有効であり、closed はブール値を返すため、そのコンテキストが適切であるかを確認しています。
    • walktype(n->left, Erv): n->leftclose または 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ノードを ar に代入します。
    • 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; フィールドが追加されました。このフィールドは、上記のフラグを組み合わせてチャネルのクローズ状態を保持します。elemsizeuint32 から 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 := <-chok 部分) に対応するランタイム実装です。

    • 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.」という記述を裏付けるものです。特に RclosedRmax の扱いは、後のGoのチャネルの ok idiom (v, ok := <-ch) のセマンティクスとは異なり、初期の実験的な実装であることが伺えます。最終的には、チャネルが閉じられた後に値を受信しようとすると、その型のゼロ値と false が返されるという、より洗練されたメカニズムに進化します。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (golang/go GitHubリポジトリ)
  • Go言語のコンパイラとランタイムに関する一般的な知識
  • Yacc/Bison および Lex/Flex の基本的な概念