[インデックス 1828] ファイルの概要
このコミットは、Go言語のランタイムにおけるチャネルの close および closed 操作の挙動を大幅に改善し、より堅牢なエラーハンドリングと予測可能なセマンティクスを導入しています。特に、チャネルが閉じられた際の送受信の挙動、select ステートメントの対応、そして閉じられたチャネルに対する操作の誤用検出メカニズムが強化されています。
コミット
Go言語のランタイムにおけるチャネルの close および closed 操作に関する変更。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4523ee9ac8873552d6c64472c19d68760955d4a8
元コミット内容
close/closed on chans
R=r
OCL=26281
CL=26285
---
src/runtime/chan.c | 150 +++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 117 insertions(+), 33 deletions(-)
変更の背景
Go言語の初期段階において、チャネルの close 操作とその後のチャネルの挙動は、まだ完全に洗練されていませんでした。特に、閉じられたチャネルへの送信や、閉じられたチャネルからの受信がどのように振る舞うべきか、また、それらの操作がどのようにエラーとして扱われるべきかについて、明確な定義と堅牢な実装が必要とされていました。
このコミット以前は、閉じられたチャネルに対する操作が未定義の挙動を引き起こしたり、デッドロックに陥ったりする可能性がありました。また、チャネルが閉じられたことを受信側がどのように検知するかについても、より明確なメカニズムが求められていました。
この変更は、Goのチャネルが持つ並行処理の安全性と予測可能性を向上させるための重要なステップであり、現在のGoのチャネルセマンティクスの基礎を築くものです。特に、閉じられたチャネルへの送信がパニックを引き起こす挙動や、閉じられたチャネルからの受信がゼロ値と ok 値(チャネルが閉じられたかどうかを示すブール値)を返す挙動の基盤がこのコミットによって確立されました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の基本的な概念とランタイムの内部構造に関する知識が必要です。
- Go言語のチャネル (Channels):
- Goにおけるチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送受信できます。
- バッファなしチャネル (Unbuffered Channels): 送信側と受信側が同時に準備ができていなければ、通信はブロックされます。
- バッファありチャネル (Buffered Channels): 指定された数の値をバッファに格納できます。バッファが満杯でない限り送信はブロックされず、バッファが空でない限り受信はブロックされません。
make(chan Type, capacity): チャネルを作成する関数。capacityが0の場合はバッファなしチャネル、正の数の場合はバッファありチャネルになります。ch <- value: チャネルchにvalueを送信します。value := <-ch: チャネルchから値を受信します。v, ok := <-ch: チャネルからの受信操作で、okはチャネルが閉じられていないか、または値が正常に受信された場合にtrueになります。チャネルが閉じられ、かつバッファにデータが残っていない場合はfalseになります。close(ch): チャネルchを閉じます。閉じられたチャネルへの送信はパニックを引き起こします。閉じられたチャネルからの受信は、バッファにデータが残っていればそのデータを返し、バッファが空になった後はゼロ値を返し、okはfalseになります。
selectステートメント:- 複数のチャネル操作を同時に待機し、準備ができた最初の操作を実行するためのGoの制御構造です。
case <-ch:やcase ch <- value:のように、チャネルの送受信操作を記述します。defaultケースを持つこともでき、どのチャネル操作も準備ができていない場合に即座に実行されます。
- Goランタイム (Go Runtime):
- Goプログラムの実行を管理するシステムです。スケジューラ、ガベージコレクタ、チャネルの実装などが含まれます。
- チャネルの送受信操作は、GoランタイムのC言語で書かれた部分(
src/runtime/chan.cなど)で低レベルに実装されています。 Hchan構造体: Goランタイム内部でチャネルを表す構造体です。チャネルのバッファ、送受信キュー、要素のサイズなどの情報を含みます。SudoG構造体: チャネル操作でブロックされたゴルーチン(G)を管理するための補助的な構造体です。Lock: ランタイム内部で使用されるミューテックス(排他ロック)です。チャネル操作の原子性を保証するために使用されます。throw: ランタイムエラー(パニック)を発生させるための内部関数です。sys·Gosched(): 現在のゴルーチンを一時停止し、他のゴルーチンにCPUを譲るためのランタイム関数です。ready(gp): 指定されたゴルーチンgpを実行可能状態にし、スケジューラに登録します。
このコミットは、特に src/runtime/chan.c 内の Hchan 構造体の closed フィールドのセマンティクス変更、新しいエラーカウンタの導入、そして sendchan, chanrecv, sys·closechan, sys·closedchan といったチャネル操作関数のロジック変更に焦点を当てています。
技術的詳細
このコミットは、Goランタイムの src/runtime/chan.c ファイルにおけるチャネルの close および closed 状態の管理方法を根本的に変更しています。
-
closedフラグの再定義とエラーカウンタの導入:enum定義が変更され、Hchan構造体のclosedフィールドが単なるブール値ではなく、複数のビットフラグとエラーカウンタを保持するように拡張されました。Wclosed(0x0001): 書き込み側がチャネルを閉じたことを示すフラグ。Rclosed(0x0002): 読み込み側がチャネルが閉じられたことを検知したことを示すフラグ。Eincr(0x0004): エラーカウンタをインクリメントするための値。Emax(0x0800): エラーカウンタがこの値に達すると、ランタイムパニック (throw) を発生させる閾値。- これにより、チャネルの閉じられた状態をより詳細に管理し、誤用を検出できるようになりました。
-
incerr関数の導入:static void incerr(Hchan* c)という新しいヘルパー関数が追加されました。- この関数は、チャネル
cのclosedフィールドにEincrを加算し、エラーカウンタをインクリメントします。 - もしエラーカウンタが
Emaxに達した場合、"too many operations on a closed channel"というメッセージと共にランタイムパニックを発生させます。これは、閉じられたチャネルに対する過度な操作(例えば、閉じられたチャネルへの複数回の送信試行)を検出するための重要なメカニズムです。
-
sendchan(チャネル送信) の挙動変更:- チャネル送信操作 (
sendchan) の開始時と、バッファありチャネルでの非同期送信ループ (asynchラベル内) の両方で、c->closed & Wclosedのチェックが追加されました。 - もしチャネルが書き込み用に閉じられている場合、
closed:ラベルにジャンプします。 closed:ラベルでは、incerr(c)を呼び出してエラーカウンタをインクリメントし、pres(送信が成功したかどうかを示すブールポインタ) がnilでない場合は*pres = falseを設定します。- これにより、閉じられたチャネルへの送信は即座に失敗し、エラーカウンタがインクリメントされるようになりました。これは、後のGoバージョンで閉じられたチャネルへの送信がパニックを引き起こす挙動の基礎となります。
- チャネル送信操作 (
-
chanrecv(チャネル受信) の挙動変更:- チャネル受信操作 (
chanrecv) の開始時と、バッファありチャネルでの非同期受信ループ (asynchラベル内) の両方で、c->closed & Wclosedのチェックが追加されました。 - もしチャネルが書き込み用に閉じられている場合、
closed:ラベルにジャンプします。 closed:ラベルでは、受信バッファから要素をコピーする代わりにnil(ゼロ値) をep(要素ポインタ) にコピーします。c->closed |= Rclosedを設定し、読み込み側がチャネルが閉じられたことを検知したことを記録します。incerr(c)を呼び出してエラーカウンタをインクリメントし、pres(受信が成功したかどうかを示すブールポインタ) がnilでない場合は*pres = falseを設定します。- これにより、閉じられたチャネルからの受信は、バッファが空になった後にゼロ値を返し、チャネルが閉じられたことを示す
falseを返すようになりました。
- チャネル受信操作 (
-
selectステートメントの対応 (loop,gotr,gots内):selectステートメント内のチャネル操作も、Wclosedフラグのチェックを含むように変更されました。- 閉じられたチャネルに対する
selectのsendケース (gots) やrecvケース (gotr) も、それぞれincerr(c)を呼び出し、適切な挙動(ゼロ値の返却や失敗の通知)を行うように修正されました。これにより、selectを使用した場合でも、閉じられたチャネルのセマンティクスが一貫して適用されるようになりました。
-
sys·closechan(チャネルを閉じる) の挙動変更:sys·closechan関数は、チャネルを閉じる際にincerr(c)を呼び出すようになりました。- 最も重要な変更は、チャネルを閉じた際に、そのチャネルでブロックされているすべての受信ゴルーチン (
recvq) と送信ゴルーチン (sendq) を明示的に解放するループが追加されたことです。 - 解放されたゴルーチンは
ready(gp)を呼び出すことで実行可能状態に移行し、スケジューラによって再開されます。これにより、チャネルを閉じることで発生する可能性のあるデッドロックが解消され、ブロックされていたゴルーチンが適切に終了できるようになりました。
-
sys·closedchan(チャネルが閉じられたか確認) の挙動変更:sys·closedchan関数は大幅に簡素化され、c->closed & Rclosedのチェックのみを行うようになりました。- 以前存在した
RmaxやRincrといった読み込み回数に基づくエラー検出ロジックは削除されました。これは、incerr関数による汎用的なエラー検出メカニズムが導入されたため、不要になったと考えられます。
-
freesg関数の安全性向上:freesg関数にif(sg != nil)のチェックが追加され、nilポインタが渡された場合でも安全に処理されるようになりました。
これらの変更により、Goのチャネルはより堅牢で予測可能な並行処理プリミティブとなり、開発者がチャネルのライフサイクルをより安全に管理できるようになりました。特に、閉じられたチャネルへの送信がパニックを引き起こすというGoの重要なセマンティクスは、このコミットで導入されたエラー検出とゴルーチン解放のメカニズムによって実現されています。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に src/runtime/chan.c ファイル内の以下の部分に集中しています。
-
enum定義の変更:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -9,10 +9,10 @@ static Lock chanlock; enum { - Wclosed = 0x0001, - Rclosed = 0xfffe, - Rincr = 0x0002, - Rmax = 0x8000, + Wclosed = 0x0001, // writer has closed + Rclosed = 0x0002, // reader has seen close + Eincr = 0x0004, // increment errors + Emax = 0x0800, // error limit before throw }; -
Hchan構造体のclosedフィールドのコメント変更:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -41,8 +41,7 @@ struct WaitQ struct Hchan { uint16 elemsize; - uint16 closed; // Wclosed closed() hash been called - // Rclosed read-count after closed() + uint16 closed; // Wclosed Rclosed errorcount uint32 dataqsiz; // size of the circular q uint32 qcount; // total data in the q Alg* elemalg; // interface for element type -
incerr関数の追加:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -143,6 +142,16 @@ sys·newchan(uint32 elemsize, uint32 elemalg, uint32 hint, } } +static void +incerr(Hchan* c) +{ + c->closed += Eincr; + if(c->closed & Emax) { + unlock(&chanlock); + throw("too many operations on a closed channel"); + } +} -
sendchanおよびchanrecv内のclosedチェックとgoto closedロジック: (例:sendchanの一部)--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -167,9 +176,13 @@ sendchan(Hchan *c, byte *ep, bool *pres) } lock(&chanlock); +\ if(c->dataqsiz > 0) goto asynch; +\ if(c->closed & Wclosed) +\ goto closed; +\ sg = dequeue(&c->recvq, c); if(sg != nil) { if(ep != nil) @@ -238,6 +255,13 @@ asynch: unlock(&chanlock); if(pres != nil) *pres = true; +\ return; +\ +closed: +\ incerr(c); +\ if(pres != nil) +\ *pres = false; +\ unlock(&chanlock); }(例:
chanrecvの一部)--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -315,10 +353,23 @@ asynch: \tfreesg(c, sg); \tunlock(&chanlock); \tready(gp); -\ } else -\ unlock(&chanlock); +\ \tif(pres != nil) +\ \t\t*pres = true; +\ \treturn; +\ } +\ +\ unlock(&chanlock); if(pres != nil) *pres = true; +\ return; +\ +closed: +\ c->elemalg->copy(c->elemsize, ep, nil); +\ c->closed |= Rclosed; +\ incerr(c); +\ if(pres != nil) +\ *pres = false; +\ unlock(&chanlock); } -
sys·closechanのロジック変更:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -803,35 +873,47 @@ retc: void sys·closechan(Hchan *c) { -\ if(c == nil) -\ throw("closechan: channel not allocated"); -\ -\ // if wclosed already set -\ // work has been done - just return -\ if(c->closed & Wclosed) -\ return; +\ SudoG *sg; +\ G* gp; +\ -\ // set wclosed +\ lock(&chanlock); +\ incerr(c); +\ c->closed |= Wclosed; +\ +\ // release all readers +\ for(;;) { +\ sg = dequeue(&c->recvq, c); +\ if(sg == nil) +\ break; +\ gp = sg->g; +\ gp->param = nil; +\ freesg(c, sg); +\ ready(gp); +\ } +\ +\ // release all writers +\ for(;;) { +\ sg = dequeue(&c->sendq, c); +\ if(sg == nil) +\ break; +\ gp = sg->g; +\ gp->param = nil; +\ freesg(c, sg); +\ ready(gp); +\ } +\ +\ unlock(&chanlock); } -
sys·closedchanの簡素化:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -892,11 +974,13 @@ allocsg(Hchan *c) static void freesg(Hchan *c, SudoG *sg) { -\ if(sg->isfree) -\ throw("chan.freesg: already free"); -\ sg->isfree = 1; -\tsg->link = c->free; -\tc->free = sg; +\ if(sg != nil) { +\ if(sg->isfree) +\ throw("chan.freesg: already free"); +\ sg->isfree = 1; +\ sg->link = c->free; +\ c->free = sg; +\ } }
コアとなるコードの解説
-
enum定義の変更:WclosedとRclosedは、それぞれチャネルの書き込み側と読み込み側がチャネルのクローズ状態を認識したことを示すビットフラグとして明確に定義されました。EincrとEmaxは、チャネルの誤用(例えば、閉じられたチャネルへの複数回の送信試行)を検出するためのエラーカウンタのメカニズムを導入します。Eincrはカウンタの増分値、Emaxはパニックを発生させる閾値です。これにより、ランタイムはチャネルの不正な使用を検出し、早期に問題を報告できるようになります。
-
Hchan構造体のclosedフィールド:- このフィールドは、単にチャネルが閉じられたかどうかを示すだけでなく、
Wclosed,Rclosedフラグとエラーカウンタの値をビット単位で保持するようになりました。これにより、チャネルの内部状態をより詳細に表現できるようになります。
- このフィールドは、単にチャネルが閉じられたかどうかを示すだけでなく、
-
incerr関数:- この関数は、チャネルに対する不正な操作(閉じられたチャネルへの送信など)が行われた際に呼び出されます。
c->closed += Eincr;でエラーカウンタをインクリメントします。if(c->closed & Emax)のチェックにより、エラーカウンタが閾値を超えた場合にthrow("too many operations on a closed channel")を呼び出し、ランタイムパニックを発生させます。これは、Goのチャネルセマンティクスにおいて、閉じられたチャネルへの送信がパニックを引き起こす挙動の直接的な実装です。
-
sendchanおよびchanrecv内のclosedチェックとgoto closedロジック:- これらの関数は、チャネルの送受信操作の核心部分です。
if(c->closed & Wclosed)のチェックが追加されたことで、チャネルが書き込み用に閉じられている場合、送受信操作は即座にclosed:ラベルにジャンプします。closed:ラベルでは、incerr(c)を呼び出してエラーカウンタをインクリメントし、操作が失敗したことを示す*pres = falseを設定します。- 受信側では、
c->elemalg->copy(c->elemsize, ep, nil)によってゼロ値が受信バッファにコピーされ、c->closed |= Rclosedによって読み込み側がチャネルが閉じられたことを認識した状態に設定されます。 - これにより、閉じられたチャネルへの送信は失敗し、閉じられたチャネルからの受信はゼロ値を返すという、現在のGoのチャネルの挙動が確立されました。
-
sys·closechanのロジック変更:- 以前は単純に
Wclosedフラグを設定するだけでしたが、このコミットにより、チャネルを閉じる際にincerr(c)を呼び出すようになりました。 - 最も重要なのは、チャネルを閉じた際に、そのチャネルでブロックされているすべての送信ゴルーチン (
sendq) と受信ゴルーチン (recvq) を明示的に解放するループが追加されたことです。 dequeueでキューからゴルーチンを取り出し、gp->param = nilでゴルーチンのパラメータをクリアし、freesg(c, sg)でSudoG構造体を解放し、最後にready(gp)でゴルーチンを実行可能状態に戻します。- この変更により、チャネルを閉じることで、そのチャネルでブロックされていたすべてのゴルーチンが適切にアンブロックされ、デッドロックが回避されるようになりました。これは、Goのチャネルの堅牢性を大幅に向上させる重要な変更です。
- 以前は単純に
-
sys·closedchanの簡素化:- この関数は、チャネルが閉じられたことを読み込み側が認識したかどうか(
Rclosedフラグがセットされているか)を単純にチェックするようになりました。以前の複雑なエラーカウンタのロジックはincerrに集約されたため、この関数はよりシンプルになりました。
- この関数は、チャネルが閉じられたことを読み込み側が認識したかどうか(
これらの変更は、Goのチャネルが持つ並行処理の安全性と予測可能性を大幅に向上させ、現在のGoのチャネルセマンティクスの基礎を築いたと言えます。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語のチャネルに関する公式ブログ記事 (より新しい情報): https://go.dev/blog/go-concurrency-patterns-pipelines
- Go言語のソースコードリポジトリ: https://github.com/golang/go
参考にした情報源リンク
- Go言語のソースコード (
src/runtime/chan.c) - Go言語のチャネルに関する一般的な知識
- Go言語の
selectステートメントに関する一般的な知識 - Goランタイムの内部構造に関する一般的な知識
- コミットメッセージと差分情報
- Go言語の初期の設計に関する議論(必要に応じてWeb検索)