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

[インデックス 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: チャネル chvalue を送信します。
    • value := <-ch: チャネル ch から値を受信します。
    • v, ok := <-ch: チャネルからの受信操作で、ok はチャネルが閉じられていないか、または値が正常に受信された場合に true になります。チャネルが閉じられ、かつバッファにデータが残っていない場合は false になります。
    • close(ch): チャネル ch を閉じます。閉じられたチャネルへの送信はパニックを引き起こします。閉じられたチャネルからの受信は、バッファにデータが残っていればそのデータを返し、バッファが空になった後はゼロ値を返し、okfalse になります。
  • 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 状態の管理方法を根本的に変更しています。

  1. closed フラグの再定義とエラーカウンタの導入:

    • enum 定義が変更され、Hchan 構造体の closed フィールドが単なるブール値ではなく、複数のビットフラグとエラーカウンタを保持するように拡張されました。
    • Wclosed (0x0001): 書き込み側がチャネルを閉じたことを示すフラグ。
    • Rclosed (0x0002): 読み込み側がチャネルが閉じられたことを検知したことを示すフラグ。
    • Eincr (0x0004): エラーカウンタをインクリメントするための値。
    • Emax (0x0800): エラーカウンタがこの値に達すると、ランタイムパニック (throw) を発生させる閾値。
    • これにより、チャネルの閉じられた状態をより詳細に管理し、誤用を検出できるようになりました。
  2. incerr 関数の導入:

    • static void incerr(Hchan* c) という新しいヘルパー関数が追加されました。
    • この関数は、チャネル cclosed フィールドに Eincr を加算し、エラーカウンタをインクリメントします。
    • もしエラーカウンタが Emax に達した場合、"too many operations on a closed channel" というメッセージと共にランタイムパニックを発生させます。これは、閉じられたチャネルに対する過度な操作(例えば、閉じられたチャネルへの複数回の送信試行)を検出するための重要なメカニズムです。
  3. sendchan (チャネル送信) の挙動変更:

    • チャネル送信操作 (sendchan) の開始時と、バッファありチャネルでの非同期送信ループ (asynch ラベル内) の両方で、c->closed & Wclosed のチェックが追加されました。
    • もしチャネルが書き込み用に閉じられている場合、closed: ラベルにジャンプします。
    • closed: ラベルでは、incerr(c) を呼び出してエラーカウンタをインクリメントし、pres (送信が成功したかどうかを示すブールポインタ) が nil でない場合は *pres = false を設定します。
    • これにより、閉じられたチャネルへの送信は即座に失敗し、エラーカウンタがインクリメントされるようになりました。これは、後のGoバージョンで閉じられたチャネルへの送信がパニックを引き起こす挙動の基礎となります。
  4. chanrecv (チャネル受信) の挙動変更:

    • チャネル受信操作 (chanrecv) の開始時と、バッファありチャネルでの非同期受信ループ (asynch ラベル内) の両方で、c->closed & Wclosed のチェックが追加されました。
    • もしチャネルが書き込み用に閉じられている場合、closed: ラベルにジャンプします。
    • closed: ラベルでは、受信バッファから要素をコピーする代わりに nil (ゼロ値) を ep (要素ポインタ) にコピーします。
    • c->closed |= Rclosed を設定し、読み込み側がチャネルが閉じられたことを検知したことを記録します。
    • incerr(c) を呼び出してエラーカウンタをインクリメントし、pres (受信が成功したかどうかを示すブールポインタ) が nil でない場合は *pres = false を設定します。
    • これにより、閉じられたチャネルからの受信は、バッファが空になった後にゼロ値を返し、チャネルが閉じられたことを示す false を返すようになりました。
  5. select ステートメントの対応 (loop, gotr, gots 内):

    • select ステートメント内のチャネル操作も、Wclosed フラグのチェックを含むように変更されました。
    • 閉じられたチャネルに対する selectsend ケース (gots) や recv ケース (gotr) も、それぞれ incerr(c) を呼び出し、適切な挙動(ゼロ値の返却や失敗の通知)を行うように修正されました。これにより、select を使用した場合でも、閉じられたチャネルのセマンティクスが一貫して適用されるようになりました。
  6. sys·closechan (チャネルを閉じる) の挙動変更:

    • sys·closechan 関数は、チャネルを閉じる際に incerr(c) を呼び出すようになりました。
    • 最も重要な変更は、チャネルを閉じた際に、そのチャネルでブロックされているすべての受信ゴルーチン (recvq) と送信ゴルーチン (sendq) を明示的に解放するループが追加されたことです。
    • 解放されたゴルーチンは ready(gp) を呼び出すことで実行可能状態に移行し、スケジューラによって再開されます。これにより、チャネルを閉じることで発生する可能性のあるデッドロックが解消され、ブロックされていたゴルーチンが適切に終了できるようになりました。
  7. sys·closedchan (チャネルが閉じられたか確認) の挙動変更:

    • sys·closedchan 関数は大幅に簡素化され、c->closed & Rclosed のチェックのみを行うようになりました。
    • 以前存在した RmaxRincr といった読み込み回数に基づくエラー検出ロジックは削除されました。これは、incerr 関数による汎用的なエラー検出メカニズムが導入されたため、不要になったと考えられます。
  8. freesg 関数の安全性向上:

    • freesg 関数に if(sg != nil) のチェックが追加され、nil ポインタが渡された場合でも安全に処理されるようになりました。

これらの変更により、Goのチャネルはより堅牢で予測可能な並行処理プリミティブとなり、開発者がチャネルのライフサイクルをより安全に管理できるようになりました。特に、閉じられたチャネルへの送信がパニックを引き起こすというGoの重要なセマンティクスは、このコミットで導入されたエラー検出とゴルーチン解放のメカニズムによって実現されています。

コアとなるコードの変更箇所

このコミットのコアとなる変更は、主に src/runtime/chan.c ファイル内の以下の部分に集中しています。

  1. 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
     };
    
  2. 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
    
  3. 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");
    +	}
    +}
    
  4. 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);
     }
    
  5. 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);
     }
    
  6. 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 定義の変更:

    • WclosedRclosed は、それぞれチャネルの書き込み側と読み込み側がチャネルのクローズ状態を認識したことを示すビットフラグとして明確に定義されました。
    • EincrEmax は、チャネルの誤用(例えば、閉じられたチャネルへの複数回の送信試行)を検出するためのエラーカウンタのメカニズムを導入します。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言語のソースコード (src/runtime/chan.c)
  • Go言語のチャネルに関する一般的な知識
  • Go言語の select ステートメントに関する一般的な知識
  • Goランタイムの内部構造に関する一般的な知識
  • コミットメッセージと差分情報
  • Go言語の初期の設計に関する議論(必要に応じてWeb検索)