[インデックス 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検索)