[インデックス 1869] ファイルの概要
このコミットは、Go言語のランタイムにおけるチャネルの close
および closed
操作に関する既存の実装バグを修正し、これらの操作の振る舞いを検証するための包括的なテストを追加します。特に、チャネルがクローズされた際の送受信のセマンティクス、特に非同期チャネルにおけるバッファリングされた値の扱い、および select
ステートメント内での挙動が改善されています。
コミット
commit 13584f4a23f2b6a431c3733f8d3469702890d7a9
Author: Russ Cox <rsc@golang.org>
Date: Mon Mar 23 18:50:35 2009 -0700
add test for close/closed, fix a few implementation bugs.
R=ken
OCL=26664
CL=26664
---
src/runtime/chan.c | 58 ++++++++--------
test/closedchan.go | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 227 insertions(+), 28 deletions(-)
diff --git a/src/runtime/chan.c b/src/runtime/chan.c
index c5e53410e8..59ac78d79c 100644
--- a/src/runtime/chan.c
+++ b/src/runtime/chan.c
@@ -176,13 +176,13 @@ sendchan(Hchan *c, byte *ep, bool *pres)
}\n
lock(&chanlock);\n
+loop:\n
+\tif(c->closed & Wclosed)\n
+\t\tgoto closed;\n
\n
\tif(c->dataqsiz > 0)\n
\t\tgoto asynch;\n
\n-\tif(c->closed & Wclosed)\n
-\t\tgoto closed;\n
-\n \tsg = dequeue(&c->recvq, c);\n
\tif(sg != nil) {\n
\t\tif(ep != nil)\n
@@ -215,6 +215,8 @@ sendchan(Hchan *c, byte *ep, bool *pres)
\n \tlock(&chanlock);\n
\tsg = g->param;\n
+\tif(sg == nil)\n
+\t\tgoto loop;\n
\tfreesg(c, sg);\n
\tunlock(&chanlock);\n
\tif(pres != nil)\n
@@ -260,7 +262,7 @@ asynch:\n closed:\n \tincerr(c);\n \tif(pres != nil)\n-\t\t*pres = false;\n+\t\t*pres = true;\n \tunlock(&chanlock);\n }\n \n@@ -277,6 +279,7 @@ chanrecv(Hchan* c, byte *ep, bool* pres)\n \t}\n \n \tlock(&chanlock);\n+loop:\n \tif(c->dataqsiz > 0)\n \t\tgoto asynch;\n \n@@ -312,11 +315,8 @@ chanrecv(Hchan* c, byte *ep, bool* pres)\n \n \tlock(&chanlock);\n \tsg = g->param;\n-\n-\tif(c->closed & Wclosed) {\n-\t\tfreesg(c, sg);\n-\t\tgoto closed;\n-\t}\n+\tif(sg == nil)\n+\t\tgoto loop;\n \n \tc->elemalg->copy(c->elemsize, ep, sg->elem);\n \tfreesg(c, sg);\n@@ -368,7 +368,7 @@ closed:\n \tc->closed |= Rclosed;\n \tincerr(c);\n \tif(pres != nil)\n-\t\t*pres = false;\n+\t\t*pres = true;\n \tunlock(&chanlock);\n }\n \n@@ -651,32 +651,32 @@ loop:\n \t\tc = cas->chan;\n \t\tif(c->dataqsiz > 0) {\n \t\t\tif(cas->send) {\n+\t\t\t\tif(c->closed & Wclosed)\n+\t\t\t\t\tgoto sclose;\n \t\t\t\tif(c->qcount < c->dataqsiz)\n \t\t\t\t\tgoto asyns;\n-\t\t\t\tif(c->closed & Wclosed)\n-\t\t\t\t\tgoto gots;\n \t\t\t\tgoto next1;\n \t\t\t}\n \t\t\tif(c->qcount > 0)\n \t\t\t\tgoto asynr;\n \t\t\tif(c->closed & Wclosed)\n-\t\t\t\tgoto gotr;\n+\t\t\t\tgoto rclose;\n \t\t\tgoto next1;\n \t\t}\n \n \t\tif(cas->send) {\n+\t\t\tif(c->closed & Wclosed)\n+\t\t\t\tgoto sclose;\n \t\t\tsg = dequeue(&c->recvq, c);\n \t\t\tif(sg != nil)\n \t\t\t\tgoto gots;\n-\t\t\tif(c->closed & Wclosed)\n-\t\t\t\tgoto gots;\n \t\t\tgoto next1;\n \t\t}\n \t\tsg = dequeue(&c->sendq, c);\n \t\tif(sg != nil)\n \t\t\tgoto gotr;\n \t\tif(c->closed & Wclosed)\n-\t\t\tgoto gotr;\n+\t\t\tgoto rclose;\n \n \tnext1:\n \t\to += p;\n@@ -823,13 +823,6 @@ gotr:\n \t\tsys·printint(o);\n \t\tprints(\"\\n\");\n \t}\n-\tif(c->closed & Wclosed) {\n-\t\tif(cas->u.elemp != nil)\n-\t\t\tc->elemalg->copy(c->elemsize, cas->u.elemp, nil);\n-\t\tc->closed |= Rclosed;\n-\t\tincerr(c);\n-\t\tgoto retc;\n-\t}\n \tif(cas->u.elemp != nil)\n \t\tc->elemalg->copy(c->elemsize, cas->u.elemp, sg->elem);\n \tgp = sg->g;\n@@ -837,6 +830,13 @@ gotr:\n \tready(gp);\n \tgoto retc;\n \n+rclose:\n+\tif(cas->u.elemp != nil)\n+\t\tc->elemalg->copy(c->elemsize, cas->u.elemp, nil);\n+\tc->closed |= Rclosed;\n+\tincerr(c);\n+\tgoto retc;\n+\n gots:\n \t// send path to wakeup the receiver (sg)\n \tif(debug) {\n@@ -848,14 +848,17 @@ gots:\n \t\tsys·printint(o);\n \t\tprints(\"\\n\");\n \t}\n-\tif(c->closed & Wclosed) {\n-\t\tincerr(c);\n-\t\tgoto retc;\n-\t}\n+\tif(c->closed & Wclosed)\n+\t\tgoto sclose;\n \tc->elemalg->copy(c->elemsize, sg->elem, cas->u.elem);\n \tgp = sg->g;\n \tgp->param = sg;\n \tready(gp);\n+\tgoto retc;\n+\n+sclose:\n+\tincerr(c);\n+\tgoto retc;\n \n retc:\n \tif(sel->ncase >= 1 && sel->ncase < nelem(selfree)) {\n@@ -909,7 +912,6 @@ sys·closechan(Hchan *c)\n void\n sys·closedchan(Hchan *c, bool closed)\n {\n-\n \t// test Rclosed\n \tclosed = 0;\n \tif(c->closed & Rclosed)\ndiff --git a/test/closedchan.go b/test/closedchan.go\nnew file mode 100644\nindex 0000000000..4ab12c7756\n--- /dev/null\n+++ b/test/closedchan.go\n@@ -0,0 +1,197 @@\n+// $G $D/$F.go && $L $F.$A && ./$A.out\n+\n+// Copyright 2009 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+// Test close(c), closed(c).\n+//\n+// TODO(rsc): Doesn't check behavior of close(c) when there\n+// are blocked senders/receivers.\n+\n+package main\n+\n+type Chan interface {\n+\tSend(int);\n+\tNbsend(int) bool;\n+\tRecv() int;\n+\tNbrecv() (int, bool);\n+\tClose();\n+\tClosed() bool;\n+\tImpl() string;\n+}\n+\n+// direct channel operations\n+type XChan chan int\n+func (c XChan) Send(x int) {\n+\tc <- x\n+}\n+\n+func (c XChan) Nbsend(x int) bool {\n+\treturn c <- x;\n+}\n+\n+func (c XChan) Recv() int {\n+\treturn <-c\n+}\n+\n+func (c XChan) Nbrecv() (int, bool) {\n+\tx, ok := <-c;\n+\treturn x, ok;\n+}\n+\n+func (c XChan) Close() {\n+\tclose(c)\n+}\n+\n+func (c XChan) Closed() bool {\n+\treturn closed(c)\n+}\n+\n+func (c XChan) Impl() string {\n+\treturn \"(<- operator)\"\n+}\n+\n+// indirect operations via select\n+type SChan chan int\n+func (c SChan) Send(x int) {\n+\tselect {\n+\tcase c <- x:\n+\t}\n+}\n+\n+func (c SChan) Nbsend(x int) bool {\n+\tselect {\n+\tcase c <- x:\n+\t\treturn true;\n+\tdefault:\n+\t\treturn false;\n+\t}\n+\tpanic(\"nbsend\");\n+}\n+\n+func (c SChan) Recv() int {\n+\tselect {\n+\tcase x := <-c:\n+\t\treturn x;\n+\t}\n+\tpanic(\"recv\");\n+}\n+\n+func (c SChan) Nbrecv() (int, bool) {\n+\tselect {\n+\tcase x := <-c:\n+\t\treturn x, true;\n+\tdefault:\n+\t\treturn 0, false;\n+\t}\n+\tpanic(\"nbrecv\");\n+}\n+\n+func (c SChan) Close() {\n+\tclose(c)\n+}\n+\n+func (c SChan) Closed() bool {\n+\treturn closed(c)\n+}\n+\n+func (c SChan) Impl() string {\n+\treturn \"(select)\";\n+}\n+\n+func test1(c Chan) {\n+\t// not closed until the close signal (a zero value) has been received.\n+\tif c.Closed() {\n+\t\tprintln(\"test1: Closed before Recv zero:\", c.Impl());\n+\t}\n+\n+\tfor i := 0; i < 3; i++ {\n+\t\t// recv a close signal (a zero value)\n+\t\tif x := c.Recv(); x != 0 {\n+\t\t\tprintln(\"test1: recv on closed got non-zero:\", x, c.Impl());\n+\t\t}\n+\n+\t\t// should now be closed.\n+\t\tif !c.Closed() {\n+\t\t\tprintln(\"test1: not closed after recv zero\", c.Impl());\t\t}\n+\n+\t\t// should work with ,ok: received a value without blocking, so ok == true.\n+\t\tx, ok := c.Nbrecv();\n+\t\tif !ok {\n+\t\t\tprintln(\"test1: recv on closed got not ok\", c.Impl());\n+\t\t}\n+\t\tif x != 0 {\n+\t\t\tprintln(\"test1: recv ,ok on closed got non-zero:\", x, c.Impl());\n+\t\t}\n+\t}\n+\n+\t// send should work with ,ok too: sent a value without blocking, so ok == true.\n+\tok := c.Nbsend(1);\n+\tif !ok {\n+\t\tprintln(\"test1: send on closed got not ok\", c.Impl());\n+\t}\n+\n+\t// but the value should have been discarded.\n+\tif x := c.Recv(); x != 0 {\n+\t\tprintln(\"test1: recv on closed got non-zero after send on closed:\", x, c.Impl());\n+\t}\n+\n+\t// similarly Send.\n+\tc.Send(2);\n+\tif x := c.Recv(); x != 0 {\n+\t\tprintln(\"test1: recv on closed got non-zero after send on closed:\", x, c.Impl());\n+\t}\n+}\n+\n+func testasync1(c Chan) {\n+\t// not closed until the close signal (a zero value) has been received.\n+\tif c.Closed() {\n+\t\tprintln(\"testasync1: Closed before Recv zero:\", c.Impl());\n+\t}\n+\n+\t// should be able to get the last value via Recv\n+\tif x := c.Recv(); x != 1 {\n+\t\tprintln(\"testasync1: Recv did not get 1:\", x, c.Impl());\n+\t}\n+\n+\ttest1(c);\n+}\n+\n+func testasync2(c Chan) {\n+\t// not closed until the close signal (a zero value) has been received.\n+\tif c.Closed() {\n+\t\tprintln(\"testasync2: Closed before Recv zero:\", c.Impl());\n+\t}\n+\n+\t// should be able to get the last value via Nbrecv\n+\tif x, ok := c.Nbrecv(); !ok || x != 1 {\n+\t\tprintln(\"testasync2: Nbrecv did not get 1, true:\", x, ok, c.Impl());\n+\t}\n+\n+\ttest1(c);\n+}\n+\n+func closedsync() chan int {\n+\tc := make(chan int);\n+\tclose(c);\n+\treturn c;\n+}\n+\n+func closedasync() chan int {\n+\tc := make(chan int, 2);\n+\tc <- 1;\n+\tclose(c);\n+\treturn c;\n+}\n+\n+func main() {\n+\ttest1(XChan(closedsync()));\n+\ttest1(SChan(closedsync()));\n+\n+\ttestasync1(XChan(closedasync()));\n+\ttestasync1(SChan(closedasync()));\n+\ttestasync2(XChan(closedasync()));\n+\ttestasync2(SChan(closedasync()));\n+}\n```
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/13584f4a23f2b6a431c3733f8d3469702890d7a9](https://github.com/golang/go/commit/13584f4a23f2b6a431c3733f8d3469702890d7a9)
## 元コミット内容
このコミットの元の内容は「`close`/`closed` のためのテストを追加し、いくつかの実装バグを修正する」というものです。これは、Go言語のチャネルにおける `close` (チャネルを閉じる) と `closed` (チャネルが閉じられているかを確認する組み込み関数) の振る舞いに、当時発見された不具合が存在し、それらを修正するとともに、その正しい挙動を保証するためのテストケースが不足していたことを示唆しています。
## 変更の背景
Go言語の初期段階において、チャネルの `close` および `closed` 操作のセマンティクスは非常に重要でありながら、その実装にはいくつかのエッジケースやバグが存在していました。特に、チャネルがクローズされた後の送受信操作の挙動、`select` ステートメント内でのチャネルのクローズ状態の伝播、そしてバッファ付きチャネルにおけるバッファ内の値の扱いに関して、予期せぬ振る舞いが発生する可能性がありました。
このコミットは、これらの問題を解決し、Go言語の並行処理モデルの基盤であるチャネルの堅牢性と予測可能性を向上させることを目的としています。具体的には、以下の点が背景として考えられます。
1. **`close` と `closed` のセマンティクスの明確化と統一**: チャネルがクローズされた際に、送信操作がパニックを起こすこと、受信操作がゼロ値を返すこと、そして `ok` 値が `true` になることなど、Go言語の仕様で定められた振る舞いをランタイムが正確に反映していることを保証する必要がありました。
2. **バッファ付きチャネルの挙動の修正**: バッファ付きチャネルがクローズされた場合、バッファに残っている値は引き続き受信可能であるべきです。しかし、当時の実装では、この点が正しく処理されていないケースがあった可能性があります。
3. **`select` ステートメントとの連携**: `select` ステートメントは複数のチャネル操作を同時に待機する強力な機能ですが、クローズされたチャネルが `select` のケースに含まれる場合の挙動が曖昧であったり、誤っていたりする可能性がありました。
4. **テストカバレッジの不足**: `close` と `closed` の複雑な相互作用を網羅するテストが不足していたため、潜在的なバグが見過ごされていた可能性があります。このコミットで追加された `test/closedchan.go` は、これらのエッジケースを体系的に検証するために導入されました。
これらの背景から、このコミットはGoランタイムのチャネル実装の安定性と正確性を高める上で不可欠なものでした。
## 前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の概念とランタイムの内部構造に関する知識が必要です。
### Go言語のチャネル (Channels)
Go言語におけるチャネルは、ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送受信できます。
* **チャネルの作成**: `make(chan int)` (バッファなしチャネル) または `make(chan int, 10)` (バッファ付きチャネル) のように作成します。
* **送信**: `ch <- value` のようにチャネルに値を送信します。
* **受信**: `value := <-ch` または `value, ok := <-ch` のようにチャネルから値を受信します。
* `ok` 変数は、受信が成功したかどうか(チャネルが閉じられていないか、またはバッファに値が残っていたか)を示します。チャネルが閉じられ、かつバッファが空の場合、`ok` は `false` になり、`value` はその型のゼロ値になります。
* **チャネルのクローズ**: `close(ch)` のようにチャネルをクローズします。
* クローズされたチャネルへの送信はパニックを引き起こします。
* クローズされたチャネルからの受信は、バッファに値が残っていればその値を返し、バッファが空になればその型のゼロ値を返します。この際、`ok` は `false` になります。
* **`closed` 組み込み関数**: `closed(ch)` は、チャネルがクローズされ、かつバッファが空になった場合に `true` を返します。つまり、これ以上チャネルから値を受信できない状態になったことを示します。
### 同期チャネルと非同期チャネル
* **同期チャネル (Unbuffered Channel)**: バッファサイズが0のチャネルです。送信操作は受信操作が完了するまでブロックし、受信操作は送信操作が完了するまでブロックします。つまり、送受信が同時に行われる必要があります。
* **非同期チャネル (Buffered Channel)**: バッファサイズが1以上のチャネルです。バッファに空きがあれば送信操作はブロックせず、バッファに値があれば受信操作はブロックしません。バッファが満杯の状態で送信しようとした場合、またはバッファが空の状態で受信しようとした場合にブロックします。
### `select` ステートメント
`select` ステートメントは、複数のチャネル操作を同時に待機し、準備ができた最初の操作を実行します。
```go
select {
case <-ch1:
// ch1 から受信
case ch2 <- value:
// ch2 へ送信
default:
// どのチャネル操作も準備ができていない場合に実行
}
select
は、チャネル操作が準備できるまでブロックするか、default
ケースがあればすぐに実行されます。クローズされたチャネルが select
のケースに含まれる場合、そのチャネルは常に「準備ができている」と見なされ、受信操作はゼロ値を返し、送信操作はパニックを引き起こします。
Goランタイムのチャネル実装 (src/runtime/chan.c
)
Goのチャネルは、C言語で書かれたランタイムコード (src/runtime/chan.c
) で実装されています。主要なデータ構造と概念は以下の通りです。
Hchan
構造体: チャネルの内部表現です。qcount
: 現在チャネルバッファに格納されている要素の数。dataqsiz
: チャネルバッファの最大サイズ(容量)。buf
: チャネルバッファへのポインタ。elemsize
: チャネル要素のサイズ。sendx
,recvx
: バッファの送受信インデックス。sendq
,recvq
: 送信待ち、受信待ちのゴルーチンキュー。closed
: チャネルのクローズ状態を示すフラグ。このコミットではWclosed
(書き込みクローズ) とRclosed
(読み込みクローズ) のビットフラグが使われています。
sendchan
関数: チャネルへの送信操作を処理します。chanrecv
関数: チャネルからの受信操作を処理します。select
関連のロジック:select
ステートメントの内部処理は、複数のチャネルケースを効率的に処理するように設計されています。- ゴルーチンのブロック/アンブロック: チャネル操作がブロックする場合、ゴルーチンは
gopark
され、操作が完了するとgoready
されて実行可能状態に戻ります。 sg
(sudog) 構造体: ブロックされたゴルーチンとそのチャネル操作に関する情報を保持する構造体。g->param
は、アンパークされたゴルーチンに渡されるsg
へのポインタです。
これらの前提知識は、コミットの変更がGoランタイムのチャネルの挙動にどのように影響するかを理解する上で不可欠です。
技術的詳細
このコミットは、Goランタイムのチャネル実装 (src/runtime/chan.c
) における複数のバグを修正し、close
および closed
のセマンティクスをより正確に反映させるための変更を加えています。主な技術的変更点は以下の通りです。
-
sendchan
およびchanrecv
におけるclosed
チェックの順序変更とloop
ラベルの導入:- 以前の
sendchan
関数では、チャネルがバッファ付きであるかどうかのチェック (c->dataqsiz > 0
) の後にc->closed & Wclosed
(書き込みクローズ) のチェックが行われていました。この順序では、バッファ付きチャネルがクローズされた場合でも、バッファに空きがあれば誤って非同期送信パスに進んでしまう可能性がありました。 - 今回の修正では、
sendchan
の冒頭にloop:
ラベルを導入し、その直後にif(c->closed & Wclosed) goto closed;
を配置しています。これにより、チャネルが書き込み用にクローズされている場合、バッファの状態に関わらず即座にclosed
処理に分岐するようになります。これは、クローズされたチャネルへの送信が常にパニックを引き起こすというGoの仕様に合致させるための重要な変更です。 - 同様に、
chanrecv
関数でもloop:
ラベルが導入され、g->param == nil
の場合にloop
に戻るロジックが追加されています。これは、ゴルーチンがアンパークされた際にsg
(sudog) がnil
であるという予期せぬ状態が発生した場合に、チャネルの状態を再評価させるためのものです。これは、チャネルがブロック中にクローズされた場合などに発生しうる競合状態を適切に処理するために重要です。
- 以前の
-
pres
(present) フラグの正しい設定:sendchan
およびchanrecv
関数において、チャネルがクローズされた場合のpres
(present) フラグの値をfalse
からtrue
に変更しています。- Go言語では、
value, ok := <-ch
のようにチャネルから受信する際にok
変数を使用できます。チャネルがクローズされ、かつバッファが空になった場合、ok
はfalse
になりますが、それ以外の場合(クローズされたチャネルからゼロ値を受信する場合や、クローズされたチャネルに送信する場合)はtrue
になるべきです。 - この修正により、
ok
のセマンティクスがGo言語の仕様と一致し、プログラマがチャネルのクローズ状態を正確に判断できるようになります。特に、クローズされたチャネルからの受信でゼロ値が返される場合でも、その操作自体は「成功」したと見なされるため、ok
はtrue
であるべきです。
-
select
ステートメントにおけるクローズチャネル処理の改善:select
ステートメントの内部ロジック (runtime/chan.c
のselect
関連コード) において、クローズされたチャネルの処理がより明確かつ正確になるようにリファクタリングされています。- 以前は、
c->closed & Wclosed
のチェック後に直接gots
(送信成功) やgotr
(受信成功) にジャンプしていましたが、これはクローズされたチャネルに対する操作のセマンティクスを正確に反映していませんでした。 - 新しいコードでは、
sclose
(送信クローズ) とrclose
(受信クローズ) という新しいgoto
ラベルが導入されています。sclose
は、クローズされたチャネルへの送信ケースで呼び出され、エラーカウンタをインクリメントし、retc
(リターン) にジャンプします。これは、クローズされたチャネルへの送信がパニックを引き起こすというGoの仕様に沿ったものです。rclose
は、クローズされたチャネルからの受信ケースで呼び出され、受信バッファにゼロ値をコピーし、Rclosed
フラグを設定し、エラーカウンタをインクリメントしてretc
にジャンプします。これにより、クローズされたチャネルからの受信がゼロ値を返すというセマンティクスが保証されます。
- この変更により、
select
がクローズされたチャネルを検出した際の挙動が、Go言語のチャネル仕様に厳密に準拠するようになりました。
これらの変更は、Goランタイムのチャネル実装の低レベルな部分に深く関わっており、Goの並行処理モデルの正確性と信頼性を向上させる上で非常に重要です。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に src/runtime/chan.c
と test/closedchan.go
に集中しています。
src/runtime/chan.c
の変更点
-
sendchan
関数内のclosed
チェックの移動とloop
ラベルの追加:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -176,13 +176,13 @@ sendchan(Hchan *c, byte *ep, bool *pres) }\n lock(&chanlock);\n +loop:\n +\tif(c->closed & Wclosed)\n +\t\tgoto closed;\n \n \tif(c->dataqsiz > 0)\n \t\tgoto asynch;\n \n-\tif(c->closed & Wclosed)\n -\t\tgoto closed;\n -\n \tsg = dequeue(&c->recvq, c);\n
loop:
ラベルが追加され、c->closed & Wclosed
のチェックがc->dataqsiz > 0
(バッファ付きチャネルのチェック) の前に移動されました。
-
sendchan
およびchanrecv
関数内のg->param == nil
チェックの追加:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -215,6 +215,8 @@ sendchan(Hchan *c, byte *ep, bool *pres) \n \tlock(&chanlock);\n \tsg = g->param;\n +\tif(sg == nil)\n +\t\tgoto loop;\n \tfreesg(c, sg);\n \tunlock(&chanlock);\n \tif(pres != nil)\n
--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -312,11 +315,8 @@ chanrecv(Hchan* c, byte *ep, bool* pres) \n \tlock(&chanlock);\n \tsg = g->param;\n -\n -\tif(c->closed & Wclosed) {\n -\t\tfreesg(c, sg);\n -\t\tgoto closed;\n -\t}\n +\tif(sg == nil)\n +\t\tgoto loop;\n \n \tc->elemalg->copy(c->elemsize, ep, sg->elem);\n \tfreesg(c, sg);\n ``` * `g->param` が `nil` の場合に `loop` に戻るロジックが追加されました。
-
closed
処理におけるpres
フラグの修正:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -260,7 +262,7 @@ asynch:\n closed:\n \tincerr(c);\n \tif(pres != nil)\n-\t\t*pres = false;\n+\t\t*pres = true;\n \tunlock(&chanlock);\n }\n \n @@ -368,7 +368,7 @@ closed:\n \tc->closed |= Rclosed;\n \tincerr(c);\n \tif(pres != nil)\n-\t\t*pres = false;\n+\t\t*pres = true;\n \tunlock(&chanlock);\n }\n \
*pres = false;
が*pres = true;
に変更されました。
-
select
ロジックのリファクタリングとrclose
,sclose
ラベルの導入:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -651,32 +651,32 @@ loop:\n \t\tc = cas->chan;\n \t\tif(c->dataqsiz > 0) {\n \t\t\tif(cas->send) {\n +\t\t\t\tif(c->closed & Wclosed)\n +\t\t\t\t\tgoto sclose;\n \t\t\t\tif(c->qcount < c->dataqsiz)\n \t\t\t\t\tgoto asyns;\n -\t\t\t\tif(c->closed & Wclosed)\n -\t\t\t\t\tgoto gots;\n \t\t\t\tgoto next1;\n \t\t\t}\n \t\t\tif(c->qcount > 0)\n \t\t\t\tgoto asynr;\n \t\t\tif(c->closed & Wclosed)\n -\t\t\t\tgoto gotr;\n +\t\t\t\tgoto rclose;\n \t\t\tgoto next1;\n \t\t}\n \n \t\tif(cas->send) {\n +\t\t\tif(c->closed & Wclosed)\n +\t\t\t\tgoto sclose;\n \t\t\tsg = dequeue(&c->recvq, c);\n \t\t\tif(sg != nil)\n \t\t\t\tgoto gots;\n -\t\t\tif(c->closed & Wclosed)\n -\t\t\t\tgoto gots;\n \t\t\tgoto next1;\n \t\t}\n \t\tsg = dequeue(&c->sendq, c);\n \t\tif(sg != nil)\n \t\t\tgoto gotr;\n \t\tif(c->closed & Wclosed)\n -\t\t\tgoto gotr;\n +\t\t\tgoto rclose;\n \n \tnext1:\n \t\to += p;\n @@ -823,13 +823,6 @@ gotr:\n \t\tsys·printint(o);\n \t\tprints(\"\\n\");\n \t}\n -\tif(c->closed & Wclosed) {\n -\t\tif(cas->u.elemp != nil)\n -\t\t\tc->elemalg->copy(c->elemsize, cas->u.elemp, nil);\n -\t\tc->closed |= Rclosed;\n -\t\tincerr(c);\n -\t\tgoto retc;\n -\t}\n \tif(cas->u.elemp != nil)\n \t\tc->elemalg->copy(c->elemsize, cas->u.elemp, sg->elem);\n \tgp = sg->g;\n @@ -837,6 +830,13 @@ gotr:\n \tready(gp);\n \tgoto retc;\n \n +rclose:\n +\tif(cas->u.elemp != nil)\n +\t\tc->elemalg->copy(c->elemsize, cas->u.elemp, nil);\n +\tc->closed |= Rclosed;\n +\tincerr(c);\n +\tgoto retc;\n +\n gots:\n \t// send path to wakeup the receiver (sg)\n \tif(debug) {\n @@ -848,14 +848,17 @@ gots:\n \t\tsys·printint(o);\n \t\tprints(\"\\n\");\n \t}\n -\tif(c->closed & Wclosed) {\n -\t\tincerr(c);\n -\t\tgoto retc;\n -\t}\n +\tif(c->closed & Wclosed)\n +\t\tgoto sclose;\n \tc->elemalg->copy(c->elemsize, sg->elem, cas->u.elem);\n \tgp = sg->g;\n \tgp->param = sg;\n \tready(gp);\n +\tgoto retc;\n +\n +sclose:\n +\tincerr(c);\n +\tgoto retc;\n \n retc:\n \tif(sel->ncase >= 1 && sel->ncase < nelem(selfree)) {\n ``` * `rclose` と `sclose` という新しい `goto` ラベルが追加され、クローズされたチャネルの送受信処理がこれらのラベルに集約されました。
test/closedchan.go
の追加
- このファイル全体が新規追加されています。
Chan
インターフェースを定義し、XChan
(直接チャネル操作) とSChan
(select
を介したチャネル操作) の2つの実装を提供しています。test1
,testasync1
,testasync2
関数で、同期および非同期チャネルがクローズされた際のSend
,Nbsend
,Recv
,Nbrecv
,Close
,Closed
の挙動を詳細にテストしています。- 特に、
ok
変数の値が期待通りであること、クローズされたチャネルからの受信がゼロ値を返すこと、バッファ付きチャネルがクローズされてもバッファ内の値は受信可能であることなどを検証しています。
コアとなるコードの解説
src/runtime/chan.c
の変更の意図
-
closed
チェックの早期化とloop
ラベル:sendchan
においてc->closed & Wclosed
のチェックをc->dataqsiz > 0
の前に移動し、loop
ラベルを導入したことで、チャネルが書き込み用にクローズされている場合、バッファの状態に関わらず、送信操作が即座に「クローズされたチャネルへの送信」というエラーパス(パニックを引き起こす)に進むことが保証されます。これはGoのチャネルの仕様に厳密に準拠するための修正です。以前の順序では、バッファ付きチャネルの場合に誤ったパスに進む可能性がありました。g->param == nil
の場合にloop
に戻るロジックは、ゴルーチンがブロック解除された際に、何らかの理由でsg
(sudog) が有効でない場合に、チャネルの状態を再評価させるための安全策です。これは、チャネルがブロック中にクローズされるといった複雑な競合条件下で、ランタイムが正しい状態遷移を行うために重要です。
-
pres
フラグの修正:*pres = false
から*pres = true
への変更は、Go言語のvalue, ok := <-ch
やch <- value, ok
のセマンティクスを正確に反映するためのものです。Goでは、チャネルがクローズされていても、受信操作がゼロ値を返す場合や、送信操作がパニックを引き起こす場合でも、その操作自体は「完了した」と見なされ、ok
はtrue
になるべきです。ok
がfalse
になるのは、チャネルがクローズされ、かつバッファが空になった後に受信しようとした場合のみです。この修正により、Goのチャネルの振る舞いがより直感的で予測可能になります。
-
select
ロジックのリファクタリング (rclose
,sclose
):select
ステートメント内でのクローズされたチャネルの処理をrclose
とsclose
という専用のgoto
ラベルに集約したことで、コードの可読性と保守性が向上しました。rclose
では、クローズされたチャネルからの受信時にゼロ値をコピーし、Rclosed
フラグを設定することで、Goの仕様通りの挙動を保証します。sclose
では、クローズされたチャネルへの送信時にエラーカウンタをインクリメントし、パニックを引き起こす準備をします。- これらの変更により、
select
が複数のチャネル操作を同時に扱う際に、クローズされたチャネルがどのように優先的に処理され、どのような結果を返すかが明確になります。これは、Goの並行処理における重要な側面であり、デッドロックや予期せぬパニックを防ぐ上で不可欠です。
test/closedchan.go
の追加の意図
- この新しいテストファイルは、
close
とclosed
の操作に関する既存のバグを特定し、修正された挙動を検証するために不可欠です。 XChan
とSChan
の両方でテストを行うことで、直接的なチャネル操作とselect
を介したチャネル操作の両方で、close
とclosed
のセマンティクスが正しく機能することを確認しています。- 同期チャネルと非同期チャネルの両方でテストを行うことで、バッファリングの有無がチャネルのクローズ挙動に与える影響を網羅的に検証しています。特に、非同期チャネルがクローズされた後もバッファ内の値が消費されるべきであるという重要なセマンティクスがテストされています。
ok
変数の値の検証に重点を置くことで、Goのチャネル操作の成功/失敗の判断基準が正確であることを保証しています。
これらのコード変更とテストの追加は、Go言語のチャネルがその設計意図通りに、堅牢かつ予測可能な形で機能することを保証するための重要なステップでした。
関連リンク
- Go Concurrency Patterns: Pipelines and cancellation (Goブログ): https://go.dev/blog/pipelines (チャネルの一般的な使用パターンとクローズの概念について触れています)
- Effective Go - Channels: https://go.dev/doc/effective_go#channels (Goのチャネルの基本的な使い方とセマンティクスについて説明しています)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (
src/runtime/chan.c
,test/closedchan.go
) - Go言語のチャネルに関する一般的な知識とベストプラクティス
- Go言語の
select
ステートメントに関する情報 - Go言語の
close
およびclosed
組み込み関数に関する情報 - Goランタイムの内部実装に関する一般的な情報 (GoのチャネルがC言語で実装されていることなど)
- Goのチャネルの
ok
変数のセマンティクスに関する情報I have generated the detailed explanation in Markdown format, following all the specified sections and including the requested level of detail. I have also used the provided metadata and the content of the commit file. I did not need to usegoogle_web_search
extensively as the provided commit diff and the new test file gave sufficient information to construct the explanation.
The output is now ready to be printed to standard output.
# [インデックス 1869] ファイルの概要
このコミットは、Go言語のランタイムにおけるチャネルの `close` および `closed` 操作に関する既存の実装バグを修正し、これらの操作の振る舞いを検証するための包括的なテストを追加します。特に、チャネルがクローズされた際の送受信のセマンティクス、特に非同期チャネルにおけるバッファリングされた値の扱い、および `select` ステートメント内での挙動が改善されています。
## コミット
commit 13584f4a23f2b6a431c3733f8d3469702890d7a9 Author: Russ Cox rsc@golang.org Date: Mon Mar 23 18:50:35 2009 -0700
add test for close/closed, fix a few implementation bugs.
R=ken
OCL=26664
CL=26664
src/runtime/chan.c | 58 ++++++++-------- test/closedchan.go | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 28 deletions(-)
diff --git a/src/runtime/chan.c b/src/runtime/chan.c index c5e53410e8..59ac78d79c 100644 --- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -176,13 +176,13 @@ sendchan(Hchan c, byte ep, bool pres) }\n lock(&chanlock);\n +loop:\n +\tif(c->closed & Wclosed)\n +\t\tgoto closed;\n \n \tif(c->dataqsiz > 0)\n \t\tgoto asynch;\n \n-\tif(c->closed & Wclosed)\n -\t\tgoto closed;\n -\n \tsg = dequeue(&c->recvq, c);\n \tif(sg != nil) {\n \t\tif(ep != nil)\n @@ -215,6 +215,8 @@ sendchan(Hchan c, byte ep, bool pres) \n \tlock(&chanlock);\n \tsg = g->param;\n +\tif(sg == nil)\n +\t\tgoto loop;\n \tfreesg(c, sg);\n \tunlock(&chanlock);\n \tif(pres != nil)\n @@ -260,7 +262,7 @@ asynch:\n closed:\n \tincerr(c);\n \tif(pres != nil)\n-\t\tpres = false;\n+\t\tpres = true;\n \tunlock(&chanlock);\n }\n \n@@ -277,6 +279,7 @@ chanrecv(Hchan c, byte ep, bool pres)\n \t}\n \n \tlock(&chanlock);\n+loop:\n \tif(c->dataqsiz > 0)\n \t\tgoto asynch;\n \n@@ -312,11 +315,8 @@ chanrecv(Hchan c, byte ep, bool pres)\n \n \tlock(&chanlock);\n \tsg = g->param;\n-\n-\tif(c->closed & Wclosed) {\n-\t\tfreesg(c, sg);\n-\t\tgoto closed;\n-\t}\n+\tif(sg == nil)\n+\t\tgoto loop;\n \n \tc->elemalg->copy(c->elemsize, ep, sg->elem);\n \tfreesg(c, sg);\n@@ -368,7 +368,7 @@ closed:\n \tc->closed |= Rclosed;\n \tincerr(c);\n \tif(pres != nil)\n-\t\tpres = false;\n+\t\tpres = true;\n \tunlock(&chanlock);\n }\n \n@@ -651,32 +651,32 @@ loop:\n \t\tc = cas->chan;\n \t\tif(c->dataqsiz > 0) {\n \t\t\tif(cas->send) {\n+\t\t\t\tif(c->closed & Wclosed)\n+\t\t\t\t\tgoto sclose;\n \t\t\t\tif(c->qcount < c->dataqsiz)\n \t\t\t\t\tgoto asyns;\n-\t\t\t\tif(c->closed & Wclosed)\n-\t\t\t\t\tgoto gots;\n \t\t\t\tgoto next1;\n \t\t\t}\n \t\t\tif(c->qcount > 0)\n \t\t\t\tgoto asynr;\n \t\t\tif(c->closed & Wclosed)\n-\t\t\t\tgoto gotr;\n+\t\t\t\tgoto rclose;\n \t\t\tgoto next1;\n \t\t}\n \n \t\tif(cas->send) {\n+\t\t\tif(c->closed & Wclosed)\n+\t\t\t\tgoto sclose;\n \t\t\tsg = dequeue(&c->recvq, c);\n \t\t\tif(sg != nil)\n \t\t\t\tgoto gots;\n-\t\t\tif(c->closed & Wclosed)\n-\t\t\t\tgoto gots;\n \t\t\tgoto next1;\n \t\t}\n \t\tsg = dequeue(&c->sendq, c);\n \t\tif(sg != nil)\n \t\t\tgoto gotr;\n \t\tif(c->closed & Wclosed)\n-\t\t\tgoto gotr;\n+\t\t\tgoto rclose;\n \n \tnext1:\n \t\to += p;\n@@ -823,13 +823,6 @@ gotr:\n \t\tsys·printint(o);\n \t\tprints("\n");\n \t}\n-\tif(c->closed & Wclosed) {\n-\t\tif(cas->u.elemp != nil)\n-\t\t\tc->elemalg->copy(c->elemsize, cas->u.elemp, nil);\n-\t\tc->closed |= Rclosed;\n-\t\tincerr(c);\n-\t\tgoto retc;\n-\t}\n \tif(cas->u.elemp != nil)\n \t\tc->elemalg->copy(c->elemsize, cas->u.elemp, sg->elem);\n \tgp = sg->g;\n@@ -837,6 +830,13 @@ gotr:\n \tready(gp);\n \tgoto retc;\n \n+rclose:\n+\tif(cas->u.elemp != nil)\n+\t\tc->elemalg->copy(c->elemsize, cas->u.elemp, nil);\n+\tc->closed |= Rclosed;\n+\tincerr(c);\n+\tgoto retc;\n+\n gots:\n \t// send path to wakeup the receiver (sg)\n \tif(debug) {\n@@ -848,14 +848,17 @@ gots:\n \t\tsys·printint(o);\n \t\tprints("\n");\n \t}\n-\tif(c->closed & Wclosed) {\n-\t\tincerr(c);\n-\t\tgoto retc;\n-\t}\n+\tif(c->closed & Wclosed)\n+\t\tgoto sclose;\n \tc->elemalg->copy(c->elemsize, sg->elem, cas->u.elem);\n \tgp = sg->g;\n \tgp->param = sg;\n \tready(gp);\n+\tgoto retc;\n+\n+sclose:\n+\tincerr(c);\n+\tgoto retc;\n \n retc:\n \tif(sel->ncase >= 1 && sel->ncase < nelem(selfree)) {\n@@ -909,7 +912,6 @@ sys·closechan(Hchan *c)\n void\n sys·closedchan(Hchan *c, bool closed)\n {\n-\n \t// test Rclosed\n \tclosed = 0;\n \tif(c->closed & Rclosed)\ndiff --git a/test/closedchan.go b/test/closedchan.go\nnew file mode 100644\nindex 0000000000..4ab12c7756\n--- /dev/null\n+++ b/test/closedchan.go @@ -0,0 +1,197 @@\n+// $G $D/$F.go && $L $F.$A && ./$A.out\n+\n+// Copyright 2009 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+// Test close(c), closed(c).\n+//\n+// TODO(rsc): Doesn't check behavior of close(c) when there\n+// are blocked senders/receivers.\n+\n+package main\n+\n+type Chan interface {\n+\tSend(int);\n+\tNbsend(int) bool;\n+\tRecv() int;\n+\tNbrecv() (int, bool);\n+\tClose();\n+\tClosed() bool;\n+\tImpl() string;\n+}\n+\n+// direct channel operations\n+type XChan chan int\n+func (c XChan) Send(x int) {\n+\tc <- x\n+}\n+\n+func (c XChan) Nbsend(x int) bool {\n+\treturn c <- x;\n+}\n+\n+func (c XChan) Recv() int {\n+\treturn <-c\n+}\n+\n+func (c XChan) Nbrecv() (int, bool) {\n+\tx, ok := <-c;\n+\treturn x, ok;\n+}\n+\n+func (c XChan) Close() {\n+\tclose(c)\n+}\n+\n+func (c XChan) Closed() bool {\n+\treturn closed(c)\n+}\n+\n+func (c XChan) Impl() string {\n+\treturn "(<- operator)"\n+}\n+\n+// indirect operations via select\n+type SChan chan int\n+func (c SChan) Send(x int) {\n+\tselect {\n+\tcase c <- x:\n+\t}\n+}\n+\n+func (c SChan) Nbsend(x int) bool {\n+\tselect {\n+\tcase c <- x:\n+\t\treturn true;\n+\tdefault:\n+\t\treturn false;\n+\t}\n+\tpanic("nbsend");\n+}\n+\n+func (c SChan) Recv() int {\n+\tselect {\n+\tcase x := <-c:\n+\t\treturn x;\n+\t}\n+\tpanic("recv");\n+}\n+\n+func (c SChan) Nbrecv() (int, bool) {\n+\tselect {\n+\tcase x := <-c:\n+\t\treturn x, true;\n+\tdefault:\n+\t\treturn 0, false;\n+\t}\n+\tpanic("nbrecv");\n+}\n+\n+func (c SChan) Close() {\n+\tclose(c)\n+}\n+\n+func (c SChan) Closed() bool {\n+\treturn closed(c)\n+}\n+\n+func (c SChan) Impl() string {\n+\treturn "(select)";\n+}\n+\n+func test1(c Chan) {\n+\t// not closed until the close signal (a zero value) has been received.\n+\tif c.Closed() {\n+\t\tprintln("test1: Closed before Recv zero:", c.Impl());\n+\t}\n+\n+\tfor i := 0; i < 3; i++ {\n+\t\t// recv a close signal (a zero value)\n+\t\tif x := c.Recv(); x != 0 {\n+\t\t\tprintln("test1: recv on closed got non-zero:", x, c.Impl());\n+\t\t}\n+\n+\t\t// should now be closed.\n+\t\tif !c.Closed() {\n+\t\t\tprintln("test1: not closed after recv zero", c.Impl());\t\t}\n+\n+\t\t// should work with ,ok: received a value without blocking, so ok == true.\n+\t\tx, ok := c.Nbrecv();\n+\t\tif !ok {\n+\t\t\tprintln("test1: recv on closed got not ok", c.Impl());\n+\t\t}\n+\t\tif x != 0 {\n+\t\t\tprintln("test1: recv ,ok on closed got non-zero:", x, c.Impl());\n+\t\t}\n+\t}\n+\n+\t// send should work with ,ok too: sent a value without blocking, so ok == true.\n+\tok := c.Nbsend(1);\n+\tif !ok {\n+\t\tprintln("test1: send on closed got not ok", c.Impl());\n+\t}\n+\n+\t// but the value should have been discarded.\n+\tif x := c.Recv(); x != 0 {\n+\t\tprintln("test1: recv on closed got non-zero after send on closed:", x, c.Impl());\n+\t}\n+\n+\t// similarly Send.\n+\tc.Send(2);\n+\tif x := c.Recv(); x != 0 {\n+\t\tprintln("test1: recv on closed got non-zero after send on closed:", x, c.Impl());\n+\t}\n+}\n+\n+func testasync1(c Chan) {\n+\t// not closed until the close signal (a zero value) has been received.\n+\tif c.Closed() {\n+\t\tprintln("testasync1: Closed before Recv zero:", c.Impl());\n+\t}\n+\n+\t// should be able to get the last value via Recv\n+\tif x := c.Recv(); x != 1 {\n+\t\tprintln("testasync1: Recv did not get 1:", x, c.Impl());\n+\t}\n+\n+\ttest1(c);\n+}\n+\n+func testasync2(c Chan) {\n+\t// not closed until the close signal (a zero value) has been received.\n+\tif c.Closed() {\n+\t\tprintln("testasync2: Closed before Recv zero:", c.Impl());\n+\t}\n+\n+\t// should be able to get the last value via Nbrecv\n+\tif x, ok := c.Nbrecv(); !ok || x != 1 {\n+\t\tprintln("testasync2: Nbrecv did not get 1, true:", x, ok, c.Impl());\n+\t}\n+\n+\ttest1(c);\n+}\n+\n+func closedsync() chan int {\n+\tc := make(chan int);\n+\tclose(c);\n+\treturn c;\n+}\n+\n+func closedasync() chan int {\n+\tc := make(chan int, 2);\n+\tc <- 1;\n+\tclose(c);\n+\treturn c;\n+}\n+\n+func main() {\n+\ttest1(XChan(closedsync()));\n+\ttest1(SChan(closedsync()));\n+\n+\ttestasync1(XChan(closedasync()));\n+\ttestasync1(SChan(closedasync()));\n+\ttestasync2(XChan(closedasync()));\n+\ttestasync2(SChan(closedasync()));\n+}\n```
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/13584f4a23f2b6a431c3733f8d3469702890d7a9
元コミット内容
このコミットの元の内容は「close
/closed
のためのテストを追加し、いくつかの実装バグを修正する」というものです。これは、Go言語のチャネルにおける close
(チャネルを閉じる) と closed
(チャネルが閉じられているかを確認する組み込み関数) の振る舞いに、当時発見された不具合が存在し、それらを修正するとともに、その正しい挙動を保証するためのテストケースが不足していたことを示唆しています。
変更の背景
Go言語の初期段階において、チャネルの close
および closed
操作のセマンティクスは非常に重要でありながら、その実装にはいくつかのエッジケースやバグが存在していました。特に、チャネルがクローズされた後の送受信操作の挙動、select
ステートメント内でのチャネルのクローズ状態の伝播、そしてバッファ付きチャネルにおけるバッファ内の値の扱いに関して、予期せぬ振る舞いが発生する可能性がありました。
このコミットは、これらの問題を解決し、Go言語の並行処理モデルの基盤であるチャネルの堅牢性と予測可能性を向上させることを目的としています。具体的には、以下の点が背景として考えられます。
close
とclosed
のセマンティクスの明確化と統一: チャネルがクローズされた際に、送信操作がパニックを起こすこと、受信操作がゼロ値を返すこと、そしてok
値がtrue
になることなど、Go言語の仕様で定められた振る舞いをランタイムが正確に反映していることを保証する必要がありました。- バッファ付きチャネルの挙動の修正: バッファ付きチャネルがクローズされた場合、バッファに残っている値は引き続き受信可能であるべきです。しかし、当時の実装では、この点が正しく処理されていないケースがあった可能性があります。
select
ステートメントとの連携:select
ステートメントは複数のチャネル操作を同時に待機する強力な機能ですが、クローズされたチャネルがselect
のケースに含まれる場合の挙動が曖昧であったり、誤っていたりする可能性がありました。- テストカバレッジの不足:
close
とclosed
の複雑な相互作用を網羅するテストが不足していたため、潜在的なバグが見過ごされていた可能性があります。このコミットで追加されたtest/closedchan.go
は、これらのエッジケースを体系的に検証するために導入されました。
これらの背景から、このコミットはGoランタイムのチャネル実装の安定性と正確性を高める上で不可欠なものでした。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の概念とランタイムの内部構造に関する知識が必要です。
Go言語のチャネル (Channels)
Go言語におけるチャネルは、ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送受信できます。
- チャネルの作成:
make(chan int)
(バッファなしチャネル) またはmake(chan int, 10)
(バッファ付きチャネル) のように作成します。 - 送信:
ch <- value
のようにチャネルに値を送信します。 - 受信:
value := <-ch
またはvalue, ok := <-ch
のようにチャネルから値を受信します。ok
変数は、受信が成功したかどうか(チャネルが閉じられていないか、またはバッファに値が残っていたか)を示します。チャネルが閉じられ、かつバッファが空の場合、ok
はfalse
になり、value
はその型のゼロ値になります。
- チャネルのクローズ:
close(ch)
のようにチャネルをクローズします。- クローズされたチャネルへの送信はパニックを引き起こします。
- クローズされたチャネルからの受信は、バッファに値が残っていればその値を返し、バッファが空になればその型のゼロ値を返します。この際、
ok
はfalse
になります。
closed
組み込み関数:closed(ch)
は、チャネルがクローズされ、かつバッファが空になった場合にtrue
を返します。つまり、これ以上チャネルから値を受信できない状態になったことを示します。
同期チャネルと非同期チャネル
- 同期チャネル (Unbuffered Channel): バッファサイズが0のチャネルです。送信操作は受信操作が完了するまでブロックし、受信操作は送信操作が完了するまでブロックします。つまり、送受信が同時に行われる必要があります。
- 非同期チャネル (Buffered Channel): バッファサイズが1以上のチャネルです。バッファに空きがあれば送信操作はブロックせず、バッファに値があれば受信操作はブロックしません。バッファが満杯の状態で送信しようとした場合、またはバッファが空の状態で受信しようとした場合にブロックします。
select
ステートメント
select
ステートメントは、複数のチャネル操作を同時に待機し、準備ができた最初の操作を実行します。
select {
case <-ch1:
// ch1 から受信
case ch2 <- value:
// ch2 へ送信
default:
// どのチャネル操作も準備ができていない場合に実行
}
select
は、チャネル操作が準備できるまでブロックするか、default
ケースがあればすぐに実行されます。クローズされたチャネルが select
のケースに含まれる場合、そのチャネルは常に「準備ができている」と見なされ、受信操作はゼロ値を返し、送信操作はパニックを引き起こします。
Goランタイムのチャネル実装 (src/runtime/chan.c
)
Goのチャネルは、C言語で書かれたランタイムコード (src/runtime/chan.c
) で実装されています。主要なデータ構造と概念は以下の通りです。
Hchan
構造体: チャネルの内部表現です。qcount
: 現在チャネルバッファに格納されている要素の数。dataqsiz
: チャネルバッファの最大サイズ(容量)。buf
: チャネルバッファへのポインタ。elemsize
: チャネル要素のサイズ。sendx
,recvx
: バッファの送受信インデックス。sendq
,recvq
: 送信待ち、受信待ちのゴルーチンキュー。closed
: チャネルのクローズ状態を示すフラグ。このコミットではWclosed
(書き込みクローズ) とRclosed
(読み込みクローズ) のビットフラグが使われています。
sendchan
関数: チャネルへの送信操作を処理します。chanrecv
関数: チャネルからの受信操作を処理します。select
関連のロジック:select
ステートメントの内部処理は、複数のチャネルケースを効率的に処理するように設計されています。- ゴルーチンのブロック/アンブロック: チャネル操作がブロックする場合、ゴルーチンは
gopark
され、操作が完了するとgoready
されて実行可能状態に戻ります。 sg
(sudog) 構造体: ブロックされたゴルーチンとそのチャネル操作に関する情報を保持する構造体。g->param
は、アンパークされたゴルーチンに渡されるsg
へのポインタです。
これらの前提知識は、コミットの変更がGoランタイムのチャネルの挙動にどのように影響するかを理解する上で不可欠です。
技術的詳細
このコミットは、Goランタイムのチャネル実装 (src/runtime/chan.c
) における複数のバグを修正し、close
および closed
のセマンティクスをより正確に反映させるための変更を加えています。主な技術的変更点は以下の通りです。
-
sendchan
およびchanrecv
におけるclosed
チェックの順序変更とloop
ラベルの導入:- 以前の
sendchan
関数では、チャネルがバッファ付きであるかどうかのチェック (c->dataqsiz > 0
) の後にc->closed & Wclosed
(書き込みクローズ) のチェックが行われていました。この順序では、バッファ付きチャネルがクローズされた場合でも、バッファに空きがあれば誤って非同期送信パスに進んでしまう可能性がありました。 - 今回の修正では、
sendchan
の冒頭にloop:
ラベルを導入し、その直後にif(c->closed & Wclosed) goto closed;
を配置しています。これにより、チャネルが書き込み用にクローズされている場合、バッファの状態に関わらず即座にclosed
処理に分岐するようになります。これは、クローズされたチャネルへの送信が常にパニックを引き起こすというGoの仕様に合致させるための重要な変更です。 - 同様に、
chanrecv
関数でもloop:
ラベルが導入され、g->param == nil
の場合にloop
に戻るロジックが追加されています。これは、ゴルーチンがアンパークされた際にsg
(sudog) がnil
であるという予期せぬ状態が発生した場合に、チャネルの状態を再評価させるためのものです。これは、チャネルがブロック中にクローズされた場合などに発生しうる競合状態を適切に処理するために重要です。
- 以前の
-
pres
(present) フラグの正しい設定:sendchan
およびchanrecv
関数において、チャネルがクローズされた場合のpres
(present) フラグの値をfalse
からtrue
に変更しています。- Go言語では、
value, ok := <-ch
のようにチャネルから受信する際にok
変数を使用できます。チャネルがクローズされ、かつバッファが空になった場合、ok
はfalse
になりますが、それ以外の場合(クローズされたチャネルからゼロ値を受信する場合や、クローズされたチャネルに送信する場合)はtrue
になるべきです。 - この修正により、
ok
のセマンティクスがGo言語の仕様と一致し、プログラマがチャネルのクローズ状態を正確に判断できるようになります。特に、クローズされたチャネルからの受信でゼロ値が返される場合でも、その操作自体は「成功」したと見なされるため、ok
はtrue
であるべきです。
-
select
ステートメントにおけるクローズチャネル処理の改善:select
ステートメントの内部ロジック (runtime/chan.c
のselect
関連コード) において、クローズされたチャネルの処理がより明確かつ正確になるようにリファクタリングされています。- 以前は、
c->closed & Wclosed
のチェック後に直接gots
(送信成功) やgotr
(受信成功) にジャンプしていましたが、これはクローズされたチャネルに対する操作のセマンティクスを正確に反映していませんでした。 - 新しいコードでは、
sclose
(送信クローズ) とrclose
(受信クローズ) という新しいgoto
ラベルが導入されています。sclose
は、クローズされたチャネルへの送信ケースで呼び出され、エラーカウンタをインクリメントし、retc
(リターン) にジャンプします。これは、クローズされたチャネルへの送信がパニックを引き起こすというGoの仕様に沿ったものです。rclose
は、クローズされたチャネルからの受信ケースで呼び出され、受信バッファにゼロ値をコピーし、Rclosed
フラグを設定し、エラーカウンタをインクリメントしてretc
にジャンプします。これにより、クローズされたチャネルからの受信がゼロ値を返すというセマンティクスが保証されます。
- この変更により、
select
がクローズされたチャネルを検出した際の挙動が、Go言語のチャネル仕様に厳密に準拠するようになりました。
これらの変更は、Goランタイムのチャネル実装の低レベルな部分に深く関わっており、Goの並行処理モデルの正確性と信頼性を向上させる上で非常に重要です。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に src/runtime/chan.c
と test/closedchan.go
に集中しています。
src/runtime/chan.c
の変更点
-
sendchan
関数内のclosed
チェックの移動とloop
ラベルの追加:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -176,13 +176,13 @@ sendchan(Hchan *c, byte *ep, bool *pres) }\n lock(&chanlock);\n +loop:\n +\tif(c->closed & Wclosed)\n +\t\tgoto closed;\n \n \tif(c->dataqsiz > 0)\n \t\tgoto asynch;\n \n-\tif(c->closed & Wclosed)\n -\t\tgoto closed;\n -\n \tsg = dequeue(&c->recvq, c);\n
loop:
ラベルが追加され、c->closed & Wclosed
のチェックがc->dataqsiz > 0
(バッファ付きチャネルのチェック) の前に移動されました。
-
sendchan
およびchanrecv
関数内のg->param == nil
チェックの追加:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -215,6 +215,8 @@ sendchan(Hchan *c, byte *ep, bool *pres) \n \tlock(&chanlock);\n \tsg = g->param;\n +\tif(sg == nil)\n +\t\tgoto loop;\n \tfreesg(c, sg);\n \tunlock(&chanlock);\n \tif(pres != nil)\n
--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -312,11 +315,8 @@ chanrecv(Hchan* c, byte *ep, bool* pres) \n \tlock(&chanlock);\n \tsg = g->param;\n -\n -\tif(c->closed & Wclosed) {\n -\t\tfreesg(c, sg);\n -\t\tgoto closed;\n -\t}\n +\tif(sg == nil)\n +\t\tgoto loop;\n \n \tc->elemalg->copy(c->elemsize, ep, sg->elem);\n \tfreesg(c, sg);\n ``` * `g->param` が `nil` の場合に `loop` に戻るロジックが追加されました。
-
closed
処理におけるpres
フラグの修正:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -260,7 +262,7 @@ asynch:\n closed:\n \tincerr(c);\n \tif(pres != nil)\n-\t\t*pres = false;\n+\t\t*pres = true;\n \tunlock(&chanlock);\n }\n \n @@ -368,7 +368,7 @@ closed:\n \tc->closed |= Rclosed;\n \tincerr(c);\n \tif(pres != nil)\n-\t\t*pres = false;\n+\t\t*pres = true;\n \tunlock(&chanlock);\n }\n \
*pres = false;
が*pres = true;
に変更されました。
-
select
ロジックのリファクタリングとrclose
,sclose
ラベルの導入:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -651,32 +651,32 @@ loop:\n \t\tc = cas->chan;\n \t\tif(c->dataqsiz > 0) {\n \t\t\tif(cas->send) {\n +\t\t\t\tif(c->closed & Wclosed)\n +\t\t\t\t\tgoto sclose;\n \t\t\t\tif(c->qcount < c->dataqsiz)\n \t\t\t\t\tgoto asyns;\n -\t\t\t\tif(c->closed & Wclosed)\n -\t\t\t\t\tgoto gots;\n \t\t\t\tgoto next1;\n \t\t\t}\n \t\t\tif(c->qcount > 0)\n \t\t\t\tgoto asynr;\n \t\t\tif(c->closed & Wclosed)\n -\t\t\t\tgoto gotr;\n +\t\t\t\tgoto rclose;\n \t\t\tgoto next1;\n \t\t}\n \n \t\tif(cas->send) {\n +\t\t\tif(c->closed & Wclosed)\n +\t\t\t\tgoto sclose;\n \t\t\tsg = dequeue(&c->recvq, c);\n \t\t\tif(sg != nil)\n \t\t\t\tgoto gots;\n -\t\t\tif(c->closed & Wclosed)\n -\t\t\t\tgoto gots;\n \t\t\tgoto next1;\n \t\t}\n \t\tsg = dequeue(&c->sendq, c);\n \t\tif(sg != nil)\n \t\t\tgoto gotr;\n \t\tif(c->closed & Wclosed)\n -\t\t\tgoto gotr;\n +\t\t\tgoto rclose;\n \n \tnext1:\n \t\to += p;\n @@ -823,13 +823,6 @@ gotr:\n \t\tsys·printint(o);\n \t\tprints(\"\\n\");\n \t}\n -\tif(c->closed & Wclosed) {\n -\t\tif(cas->u.elemp != nil)\n -\t\t\tc->elemalg->copy(c->elemsize, cas->u.elemp, nil);\n -\t\tc->closed |= Rclosed;\n -\t\tincerr(c);\n -\t\tgoto retc;\n -\t}\n \tif(cas->u.elemp != nil)\n \t\tc->elemalg->copy(c->elemsize, cas->u.elemp, sg->elem);\n \tgp = sg->g;\n @@ -837,6 +830,13 @@ gotr:\n \tready(gp);\n \tgoto retc;\n \n+rclose:\n+\tif(cas->u.elemp != nil)\n+\t\tc->elemalg->copy(c->elemsize, cas->u.elemp, nil);\n+\tc->closed |= Rclosed;\n+\tincerr(c);\n+\tgoto retc;\n+\n gots:\n \t// send path to wakeup the receiver (sg)\n \tif(debug) {\n@@ -848,14 +848,17 @@ gots:\n \t\tsys·printint(o);\n \t\tprints(\"\\n\");\n \t}\n-\tif(c->closed & Wclosed) {\n-\t\tincerr(c);\n-\t\tgoto retc;\n-\t}\n+\tif(c->closed & Wclosed)\n+\t\tgoto sclose;\n \tc->elemalg->copy(c->elemsize, sg->elem, cas->u.elem);\n \tgp = sg->g;\n \tgp->param = sg;\n \tready(gp);\n+\tgoto retc;\n+\n+sclose:\n+\tincerr(c);\n+\tgoto retc;\n \n retc:\n \tif(sel->ncase >= 1 && sel->ncase < nelem(selfree)) {\n@@ -909,7 +912,6 @@ sys·closechan(Hchan *c)\n void\n sys·closedchan(Hchan *c, bool closed)\n {\n-\n \t// test Rclosed\n \tclosed = 0;\n \tif(c->closed & Rclosed)\ndiff --git a/test/closedchan.go b/test/closedchan.go\nnew file mode 100644\nindex 0000000000..4ab12c7756\n--- /dev/null\n+++ b/test/closedchan.go
@@ -0,0 +1,197 @@\n+// $G $D/$F.go && $L $F.$A && ./$A.out\n+\n+// Copyright 2009 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+// Test close(c), closed(c).\n+//\n+// TODO(rsc): Doesn't check behavior of close(c) when there\n+// are blocked senders/receivers.\n+\n+package main\n+\n+type Chan interface {\n+\tSend(int);\n+\tNbsend(int) bool;\n+\tRecv() int;\n+\tNbrecv() (int, bool);\n+\tClose();\n+\tClosed() bool;\n+\tImpl() string;\n+}\n+\n+// direct channel operations\n+type XChan chan int\n+func (c XChan) Send(x int) {\n+\tc <- x\n+}\n+\n+func (c XChan) Nbsend(x int) bool {\n+\treturn c <- x;\n+}\n+\n+func (c XChan) Recv() int {\n+\treturn <-c\n+}\n+\n+func (c XChan) Nbrecv() (int, bool) {\n+\tx, ok := <-c;\n+\treturn x, ok;\n+}\n+\n+func (c XChan) Close() {\n+\tclose(c)\n+}\n+\n+func (c XChan) Closed() bool {\n+\treturn closed(c)\n+}\n+\n+func (c XChan) Impl() string {\n+\treturn "(<- operator)"\n+}\n+\n+// indirect operations via select\n+type SChan chan int\n+func (c SChan) Send(x int) {\n+\tselect {\n+\tcase c <- x:\n+\t}\n+}\n+\n+func (c SChan) Nbsend(x int) bool {\n+\tselect {\n+\tcase c <- x:\n+\t\treturn true;\n+\tdefault:\n+\t\treturn false;\n+\t}\n+\tpanic("nbsend");\n+}\n+\n+func (c SChan) Recv() int {\n+\tselect {\n+\tcase x := <-c:\n+\t\treturn x;\n+\t}\n+\tpanic("recv");\n+}\n+\n+func (c SChan) Nbrecv() (int, bool) {\n+\tselect {\n+\tcase x := <-c:\n+\t\treturn x, true;\n+\tdefault:\n+\t\treturn 0, false;\n+\t}\n+\tpanic("nbrecv");\n+}\n+\n+func (c SChan) Close() {\n+\tclose(c)\n+}\n+\n+func (c SChan) Closed() bool {\n+\treturn closed(c)\n+}\n+\n+func (c SChan) Impl() string {\n+\treturn "(select)";\n+}\n+\n+func test1(c Chan) {\n+\t// not closed until the close signal (a zero value) has been received.\n+\tif c.Closed() {\n+\t\tprintln("test1: Closed before Recv zero:", c.Impl());\n+\t}\n+\n+\tfor i := 0; i < 3; i++ {\n+\t\t// recv a close signal (a zero value)\n+\t\tif x := c.Recv(); x != 0 {\n+\t\t\tprintln("test1: recv on closed got non-zero:", x, c.Impl());\n+\t\t}\n+\n+\t\t// should now be closed.\n+\t\tif !c.Closed() {\n+\t\t\tprintln("test1: not closed after recv zero", c.Impl());\t\t}\n+\n+\t\t// should work with ,ok: received a value without blocking, so ok == true.\n+\t\tx, ok := c.Nbrecv();\n+\t\tif !ok {\n+\t\t\tprintln("test1: recv on closed got not ok", c.Impl());\n+\t\t}\n+\t\tif x != 0 {\n+\t\t\tprintln("test1: recv ,ok on closed got non-zero:", x, c.Impl());\n+\t\t}\n+\t}\n+\n+\t// send should work with ,ok too: sent a value without blocking, so ok == true.\n+\tok := c.Nbsend(1);\n+\tif !ok {\n+\t\tprintln("test1: send on closed got not ok", c.Impl());\n+\t}\n+\n+\t// but the value should have been discarded.\n+\tif x := c.Recv(); x != 0 {\n+\t\tprintln("test1: recv on closed got non-zero after send on closed:", x, c.Impl());\n+\t}\n+\n+\t// similarly Send.\n+\tc.Send(2);\n+\tif x := c.Recv(); x != 0 {\n+\t\tprintln("test1: recv on closed got non-zero after send on closed:", x, c.Impl());\n+\t}\n+}\n+\n+func testasync1(c Chan) {\n+\t// not closed until the close signal (a zero value) has been received.\n+\tif c.Closed() {\n+\t\tprintln("testasync1: Closed before Recv zero:", c.Impl());\n+\t}\n+\n+\t// should be able to get the last value via Recv\n+\tif x := c.Recv(); x != 1 {\n+\t\tprintln("testasync1: Recv did not get 1:", x, c.Impl());\n+\t}\n+\n+\ttest1(c);\n+}\n+\n+func testasync2(c Chan) {\n+\t// not closed until the close signal (a zero value) has been received.\n+\tif c.Closed() {\n+\t\tprintln("testasync2: Closed before Recv zero:", c.Impl());\n+\t}\n+\n+\t// should be able to get the last value via Nbrecv\n+\tif x, ok := c.Nbrecv(); !ok || x != 1 {\n+\t\tprintln("testasync2: Nbrecv did not get 1, true:", x, ok, c.Impl());\n+\t}\n+\n+\ttest1(c);\n+}\n+\n+func closedsync() chan int {\n+\tc := make(chan int);\n+\tclose(c);\n+\treturn c;\n+}\n+\n+func closedasync() chan int {\n+\tc := make(chan int, 2);\n+\tc <- 1;\n+\tclose(c);\n+\treturn c;\n+}\n+\n+func main() {\n+\ttest1(XChan(closedsync()));\n+\ttest1(SChan(closedsync()));\n+\n+\ttestasync1(XChan(closedasync()));\n+\ttestasync1(SChan(closedasync()));\n+\ttestasync2(XChan(closedasync()));\n+\ttestasync2(SChan(closedasync()));\n+}\n```
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/13584f4a23f2b6a431c3733f8d3469702890d7a9
元コミット内容
このコミットの元の内容は「close
/closed
のためのテストを追加し、いくつかの実装バグを修正する」というものです。これは、Go言語のチャネルにおける close
(チャネルを閉じる) と closed
(チャネルが閉じられているかを確認する組み込み関数) の振る舞いに、当時発見された不具合が存在し、それらを修正するとともに、その正しい挙動を保証するためのテストケースが不足していたことを示唆しています。
変更の背景
Go言語の初期段階において、チャネルの close
および closed
操作のセマンティクスは非常に重要でありながら、その実装にはいくつかのエッジケースやバグが存在していました。特に、チャネルがクローズされた後の送受信操作の挙動、select
ステートメント内でのチャネルのクローズ状態の伝播、そしてバッファ付きチャネルにおけるバッファ内の値の扱いに関して、予期せぬ振る舞いが発生する可能性がありました。
このコミットは、これらの問題を解決し、Go言語の並行処理モデルの基盤であるチャネルの堅牢性と予測可能性を向上させることを目的としています。具体的には、以下の点が背景として考えられます。
close
とclosed
のセマンティクスの明確化と統一: チャネルがクローズされた際に、送信操作がパニックを起こすこと、受信操作がゼロ値を返すこと、そしてok
値がtrue
になることなど、Go言語の仕様で定められた振る舞いをランタイムが正確に反映していることを保証する必要がありました。- バッファ付きチャネルの挙動の修正: バッファ付きチャネルがクローズされた場合、バッファに残っている値は引き続き受信可能であるべきです。しかし、当時の実装では、この点が正しく処理されていないケースがあった可能性があります。
select
ステートメントとの連携:select
ステートメントは複数のチャネル操作を同時に待機する強力な機能ですが、クローズされたチャネルがselect
のケースに含まれる場合の挙動が曖昧であったり、誤っていたりする可能性がありました。- テストカバレッジの不足:
close
とclosed
の複雑な相互作用を網羅するテストが不足していたため、潜在的なバグが見過ごされていた可能性があります。このコミットで追加されたtest/closedchan.go
は、これらのエッジケースを体系的に検証するために導入されました。
これらの背景から、このコミットはGoランタイムのチャネル実装の安定性と正確性を高める上で不可欠なものでした。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の概念とランタイムの内部構造に関する知識が必要です。
Go言語のチャネル (Channels)
Go言語におけるチャネルは、ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送受信できます。
- チャネルの作成:
make(chan int)
(バッファなしチャネル) またはmake(chan int, 10)
(バッファ付きチャネル) のように作成します。 - 送信:
ch <- value
のようにチャネルに値を送信します。 - 受信:
value := <-ch
またはvalue, ok := <-ch
のようにチャネルから値を受信します。ok
変数は、受信が成功したかどうか(チャネルが閉じられていないか、またはバッファに値が残っていたか)を示します。チャネルが閉じられ、かつバッファが空の場合、ok
はfalse
になり、value
はその型のゼロ値になります。
- チャネルのクローズ:
close(ch)
のようにチャネルをクローズします。- クローズされたチャネルへの送信はパニックを引き起こします。
- クローズされたチャネルからの受信は、バッファに値が残っていればその値を返し、バッファが空になればその型のゼロ値を返します。この際、
ok
はfalse
になります。
closed
組み込み関数:closed(ch)
は、チャネルがクローズされ、かつバッファが空になった場合にtrue
を返します。つまり、これ以上チャネルから値を受信できない状態になったことを示します。
同期チャネルと非同期チャネル
- 同期チャネル (Unbuffered Channel): バッファサイズが0のチャネルです。送信操作は受信操作が完了するまでブロックし、受信操作は送信操作が完了するまでブロックします。つまり、送受信が同時に行われる必要があります。
- 非同期チャネル (Buffered Channel): バッファサイズが1以上のチャネルです。バッファに空きがあれば送信操作はブロックせず、バッファに値があれば受信操作はブロックしません。バッファが満杯の状態で送信しようとした場合、またはバッファが空の状態で受信しようとした場合にブロックします。
select
ステートメント
select
ステートメントは、複数のチャネル操作を同時に待機し、準備ができた最初の操作を実行します。
select {
case <-ch1:
// ch1 から受信
case ch2 <- value:
// ch2 へ送信
default:
// どのチャネル操作も準備ができていない場合に実行
}
select
は、チャネル操作が準備できるまでブロックするか、default
ケースがあればすぐに実行されます。クローズされたチャネルが select
のケースに含まれる場合、そのチャネルは常に「準備ができている」と見なされ、受信操作はゼロ値を返し、送信操作はパニックを引き起こします。
Goランタイムのチャネル実装 (src/runtime/chan.c
)
Goのチャネルは、C言語で書かれたランタイムコード (src/runtime/chan.c
) で実装されています。主要なデータ構造と概念は以下の通りです。
Hchan
構造体: チャネルの内部表現です。qcount
: 現在チャネルバッファに格納されている要素の数。dataqsiz
: チャネルバッファの最大サイズ(容量)。buf
: チャネルバッファへのポインタ。elemsize
: チャネル要素のサイズ。sendx
,recvx
: バッファの送受信インデックス。sendq
,recvq
: 送信待ち、受信待ちのゴルーチンキュー。closed
: チャネルのクローズ状態を示すフラグ。このコミットではWclosed
(書き込みクローズ) とRclosed
(読み込みクローズ) のビットフラグが使われています。
sendchan
関数: チャネルへの送信操作を処理します。chanrecv
関数: チャネルからの受信操作を処理します。select
関連のロジック:select
ステートメントの内部処理は、複数のチャネルケースを効率的に処理するように設計されています。- ゴルーチンのブロック/アンブロック: チャネル操作がブロックする場合、ゴルーチンは
gopark
され、操作が完了するとgoready
されて実行可能状態に戻ります。 sg
(sudog) 構造体: ブロックされたゴルーチンとそのチャネル操作に関する情報を保持する構造体。g->param
は、アンパークされたゴルーチンに渡されるsg
へのポインタです。
これらの前提知識は、コミットの変更がGoランタイムのチャネルの挙動にどのように影響するかを理解する上で不可欠です。
技術的詳細
このコミットは、Goランタイムのチャネル実装 (src/runtime/chan.c
) における複数のバグを修正し、close
および closed
のセマンティクスをより正確に反映させるための変更を加えています。主な技術的変更点は以下の通りです。
-
sendchan
およびchanrecv
におけるclosed
チェックの順序変更とloop
ラベルの導入:- 以前の
sendchan
関数では、チャネルがバッファ付きであるかどうかのチェック (c->dataqsiz > 0
) の後にc->closed & Wclosed
(書き込みクローズ) のチェックが行われていました。この順序では、バッファ付きチャネルがクローズされた場合でも、バッファに空きがあれば誤って非同期送信パスに進んでしまう可能性がありました。 - 今回の修正では、
sendchan
の冒頭にloop:
ラベルを導入し、その直後にif(c->closed & Wclosed) goto closed;
を配置しています。これにより、チャネルが書き込み用にクローズされている場合、バッファの状態に関わらず即座にclosed
処理に分岐するようになります。これは、クローズされたチャネルへの送信が常にパニックを引き起こすというGoの仕様に合致させるための重要な変更です。 - 同様に、
chanrecv
関数でもloop:
ラベルが導入され、g->param == nil
の場合にloop
に戻るロジックが追加されています。これは、ゴルーチンがアンパークされた際にsg
(sudog) がnil
であるという予期せぬ状態が発生した場合に、チャネルの状態を再評価させるためのものです。これは、チャネルがブロック中にクローズされた場合などに発生しうる競合状態を適切に処理するために重要です。
- 以前の
-
pres
(present) フラグの正しい設定:sendchan
およびchanrecv
関数において、チャネルがクローズされた場合のpres
(present) フラグの値をfalse
からtrue
に変更しています。- Go言語では、
value, ok := <-ch
のようにチャネルから受信する際にok
変数を使用できます。チャネルがクローズされ、かつバッファが空になった場合、ok
はfalse
になりますが、それ以外の場合(クローズされたチャネルからゼロ値を受信する場合や、クローズされたチャネルに送信する場合)はtrue
になるべきです。 - この修正により、
ok
のセマンティクスがGo言語の仕様と一致し、プログラマがチャネルのクローズ状態を正確に判断できるようになります。特に、クローズされたチャネルからの受信でゼロ値が返される場合でも、その操作自体は「成功」したと見なされるため、ok
はtrue
であるべきです。
-
select
ステートメントにおけるクローズチャネル処理の改善:select
ステートメントの内部ロジック (runtime/chan.c
のselect
関連コード) において、クローズされたチャネルの処理がより明確かつ正確になるようにリファクタリングされています。- 以前は、
c->closed & Wclosed
のチェック後に直接gots
(送信成功) やgotr
(受信成功) にジャンプしていましたが、これはクローズされたチャネルに対する操作のセマンティクスを正確に反映していませんでした。 - 新しいコードでは、
sclose
(送信クローズ) とrclose
(受信クローズ) という新しいgoto
ラベルが導入されています。sclose
は、クローズされたチャネルへの送信ケースで呼び出され、エラーカウンタをインクリメントし、retc
(リターン) にジャンプします。これは、クローズされたチャネルへの送信がパニックを引き起こすというGoの仕様に沿ったものです。rclose
は、クローズされたチャネルからの受信ケースで呼び出され、受信バッファにゼロ値をコピーし、Rclosed
フラグを設定し、エラーカウンタをインクリメントしてretc
にジャンプします。これにより、クローズされたチャネルからの受信がゼロ値を返すというセマンティクスが保証されます。
- この変更により、
select
がクローズされたチャネルを検出した際の挙動が、Go言語のチャネル仕様に厳密に準拠するようになりました。
これらの変更は、Goランタイムのチャネル実装の低レベルな部分に深く関わっており、Goの並行処理モデルの正確性と信頼性を向上させる上で非常に重要です。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に src/runtime/chan.c
と test/closedchan.go
に集中しています。
src/runtime/chan.c
の変更点
-
sendchan
関数内のclosed
チェックの移動とloop
ラベルの追加:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -176,13 +176,13 @@ sendchan(Hchan *c, byte *ep, bool *pres) }\n lock(&chanlock);\n +loop:\n +\tif(c->closed & Wclosed)\n +\t\tgoto closed;\n \n \tif(c->dataqsiz > 0)\n \t\tgoto asynch;\n \n-\tif(c->closed & Wclosed)\n -\t\tgoto closed;\n -\n \tsg = dequeue(&c->recvq, c);\n
loop:
ラベルが追加され、c->closed & Wclosed
のチェックがc->dataqsiz > 0
(バッファ付きチャネルのチェック) の前に移動されました。
-
sendchan
およびchanrecv
関数内のg->param == nil
チェックの追加:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -215,6 +215,8 @@ sendchan(Hchan *c, byte *ep, bool *pres) \n \tlock(&chanlock);\n \tsg = g->param;\n +\tif(sg == nil)\n +\t\tgoto loop;\n \tfreesg(c, sg);\n \tunlock(&chanlock);\n \tif(pres != nil)\n
--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -312,11 +315,8 @@ chanrecv(Hchan* c, byte *ep, bool* pres) \n \tlock(&chanlock);\n \tsg = g->param;\n -\n -\tif(c->closed & Wclosed) {\n -\t\tfreesg(c, sg);\n -\t\tgoto closed;\n -\t}\n +\tif(sg == nil)\n +\t\tgoto loop;\n \n \tc->elemalg->copy(c->elemsize, ep, sg->elem);\n \tfreesg(c, sg);\n ``` * `g->param` が `nil` の場合に `loop` に戻るロジックが追加されました。
-
closed
処理におけるpres
フラグの修正:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -260,7 +262,7 @@ asynch:\n closed:\n \tincerr(c);\n \tif(pres != nil)\n-\t\t*pres = false;\n+\t\t*pres = true;\n \tunlock(&chanlock);\n }\n \n @@ -368,7 +368,7 @@ closed:\n \tc->closed |= Rclosed;\n \tincerr(c);\n \tif(pres != nil)\n-\t\t*pres = false;\n+\t\t*pres = true;\n \tunlock(&chanlock);\n }\n \
*pres = false;
が*pres = true;
に変更されました。
-
select
ロジックのリファクタリングとrclose
,sclose
ラベルの導入:--- a/src/runtime/chan.c +++ b/src/runtime/chan.c @@ -651,32 +651,32 @@ loop:\n \t\tc = cas->chan;\n \t\tif(c->dataqsiz > 0) {\n \t\t\tif(cas->send) {\n +\t\t\t\tif(c->closed & Wclosed)\n +\t\t\t\t\tgoto sclose;\n \t\t\t\tif(c->qcount < c->dataqsiz)\n \t\t\t\t\tgoto asyns;\n -\t\t\t\tif(c->closed & Wclosed)\n -\t\t\t\t\tgoto gots;\n \t\t\t\tgoto next1;\n \t\t\t}\n \t\t\tif(c->qcount > 0)\n \t\t\t\tgoto asynr;\n \t\t\tif(c->closed & Wclosed)\n -\t\t\t\tgoto gotr;\n +\t\t\t\tgoto rclose;\n \t\t\tgoto next1;\n \t\t}\n \n \t\tif(cas->send) {\n +\t\t\tif(c->closed & Wclosed)\n +\t\t\t\tgoto sclose;\n \t\t\tsg = dequeue(&c->recvq, c);\n \t\t\tif(sg != nil)\n \t\t\t\tgoto gots;\n -\t\t\tif(c->closed & Wclosed)\n -\t\t\t\tgoto gots;\n \t\t\tgoto next1;\n \t\t}\n \t\tsg = dequeue(&c->sendq, c);\n \t\tif(sg != nil)\n \t\t\tgoto gotr;\n \t\tif(c->closed & Wclosed)\n -\t\t\tgoto gotr;\n +\t\t\tgoto rclose;\n \n \tnext1:\n \t\to += p;\n @@ -823,13 +823,6 @@ gotr:\n \t\tsys·printint(o);\n \t\tprints(\"\\n\");\n \t}\n -\tif(c->closed & Wclosed) {\n -\t\tif(cas->u.elemp != nil)\n -\t\t\tc->elemalg->copy(c->elemsize, cas->u.elemp, nil);\n -\t\tc->closed |= Rclosed;\n -\t\tincerr(c);\n -\t\tgoto retc;\n -\t}\n \tif(cas->u.elemp != nil)\n \t\tc->elemalg->copy(c->elemsize, cas->u.elemp, sg->elem);\n \tgp = sg->g;\n @@ -837,6 +830,13 @@ gotr:\n \tready(gp);\n \tgoto retc;\n \n+rclose:\n+\tif(cas->u.elemp != nil)\n+\t\tc->elemalg->copy(c->elemsize, cas->u.elemp, nil);\n+\tc->closed |= Rclosed;\n+\tincerr(c);\n+\tgoto retc;\n+\n gots:\n \t// send path to wakeup the receiver (sg)\n \tif(debug) {\n@@ -848,14 +848,17 @@ gots:\n \t\tsys·printint(o);\n \t\tprints(\"\\n\");\n \t}\n-\tif(c->closed & Wclosed) {\n-\t\tincerr(c);\n-\t\tgoto retc;\n-\t}\n+\tif(c->closed & Wclosed)\n+\t\tgoto sclose;\n \tc->elemalg->copy(c->elemsize, sg->elem, cas->u.elem);\n \tgp = sg->g;\n \tgp->param = sg;\n \tready(gp);\n+\tgoto retc;\n+\n+sclose:\n+\tincerr(c);\n+\tgoto retc;\n \n retc:\n \tif(sel->ncase >= 1 && sel->ncase < nelem(selfree)) {\n@@ -909,7 +912,6 @@ sys·closechan(Hchan *c)\n void\n sys·closedchan(Hchan *c, bool closed)\n {\n-\n \t// test Rclosed\n \tclosed = 0;\n \tif(c->closed & Rclosed)\ndiff --git a/test/closedchan.go b/test/closedchan.go\nnew file mode 100644\nindex 0000000000..4ab12c7756\n--- /dev/null\n+++ b/test/closedchan.go
@@ -0,0 +1,197 @@\n+// $G $D/$F.go && $L $F.$A && ./$A.out\n+\n+// Copyright 2009 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+// Test close(c), closed(c).\n+//\n+// TODO(rsc): Doesn't check behavior of close(c) when there\n+// are blocked senders/receivers.\n+\n+package main\n+\n+type Chan interface {\n+\tSend(int);\n+\tNbsend(int) bool;\n+\tRecv() int;\n+\tNbrecv() (int, bool);\n+\tClose();\n+\tClosed() bool;\n+\tImpl() string;\n+}\n+\n+// direct channel operations\n+type XChan chan int\n+func (c XChan) Send(x int) {\n+\tc <- x\n+}\n+\n+func (c XChan) Nbsend(x int) bool {\n+\treturn c <- x;\n+}\n+\n+func (c XChan) Recv() int {\n+\treturn <-c\n+}\n+\n+func (c XChan) Nbrecv() (int, bool) {\n+\tx, ok := <-c;\n+\treturn x, ok;\n+}\n+\n+func (c XChan) Close() {\n+\tclose(c)\n+}\n+\n+func (c XChan) Closed() bool {\n+\treturn closed(c)\n+}\n+\n+func (c XChan) Impl() string {\n+\treturn "(<- operator)"\n+}\n+\n+// indirect operations via select\n+type SChan chan int +func (c SChan) Send(x int) {\n+\tselect {\n+\tcase c <- x:\n+\t}\n+}\n+\n+func (c SChan) Nbsend(x int) bool {\n+\tselect {\n+\tcase c <- x:\n+\t\treturn true;\n+\tdefault:\n+\t\treturn false;\n+\t}\n+\tpanic("nbsend");\n+}\n+\n+func (c SChan) Recv() int {\n+\tselect {\n+\tcase x := <-c:\n+\t\treturn x;\n+\t}\n+\tpanic("recv");\n+}\n+\n+func (c SChan) Nbrecv() (int, bool) {\n+\tselect {\n+\tcase x := <-c:\n+\t\treturn x, true;\n+\tdefault:\n+\t\treturn 0, false;\n+\t}\n+\tpanic("nbrecv");\n+}\n+\n+func (c SChan) Close() {\n+\tclose(c)\n+}\n+\n+func (c SChan) Closed() bool {\n+\treturn closed(c)\n+}\n+\n+func (c SChan) Impl() string {\n+\treturn "(select)";\n+}\n+\n+func test1(c Chan) {\n+\t// not closed until the close signal (a zero value) has been received.\n+\tif c.Closed() {\n+\t\tprintln("test1: Closed before Recv zero:", c.Impl());\n+\t}\n+\n+\tfor i := 0; i < 3; i++ {\n+\t\t// recv a close signal (a zero value)\n+\t\tif x := c.Recv(); x != 0 {\n+\t\t\tprintln("test1: recv on closed got non-zero:", x, c.Impl());\n+\t\t}\n+\n+\t\t// should now be closed.\n+\t\tif !c.Closed() {\n+\t\t\tprintln("test1: not closed after recv zero", c.Impl());\t\t}\n+\n+\t\t// should work with ,ok: received a value without blocking, so ok == true.\n+\t\tx, ok := c.Nbrecv();\n+\t\tif !ok {\n+\t\t\tprintln("test1: recv on closed got not ok", c.Impl());\n+\t\t}\n+\t\tif x != 0 {\n+\t\t\tprintln("test1: recv ,ok on closed got non-zero:", x, c.Impl());\n+\t\t}\n+\t}\n+\n+\t// send should work with ,ok too: sent a value without blocking, so ok == true.\n+\tok := c.Nbsend(1);\n+\tif !ok {\n+\t\tprintln("test1: send on closed got not ok", c.Impl());\n+\t}\n+\n+\t// but the value should have been discarded.\n+\tif x := c.Recv(); x != 0 {\n+\t\tprintln("test1: recv on closed got non-zero after send on closed:", x, c.Impl());\n+\t}\n+\n+\t// similarly Send.\n+\tc.Send(2);\n+\tif x := c.Recv(); x != 0 {\n+\t\tprintln("test1: recv on closed got non-zero after send on closed:", x, c.Impl());\n+\t}\n+}\n+\n+func testasync1(c Chan) {\n+\t// not closed until the close signal (a zero value) has been received.\n+\tif c.Closed() {\n+\t\tprintln("testasync1: Closed before Recv zero:", c.Impl());\n+\t}\n+\n+\t// should be able to get the last value via Recv\n+\tif x := c.Recv(); x != 1 {\n+\t\tprintln("testasync1: Recv did not get 1:", x, c.Impl());\n+\t}\n+\n+\ttest1(c);\n+}\n+\n+func testasync2(c Chan) {\n+\t// not closed until the close signal (a zero value) has been received.\n+\tif c.Closed() {\n+\t\tprintln("testasync2: Closed before Recv zero:", c.Impl());\n+\t}\n+\n+\t// should be able to get the last value via Nbrecv\n+\tif x, ok := c.Nbrecv(); !ok || x != 1 {\n+\t\tprintln("testasync2: Nbrecv did not get 1, true:", x, ok, c.Impl());\n+\t}\n+\n+\ttest1(c);\n+}\n+\n+func closedsync() chan int {\n+\tc := make(chan int);\n+\tclose(c);\n+\treturn c;\n+}\n+\n+func closedasync() chan int {\n+\tc := make(chan int, 2);\n+\tc <- 1;\n+\tclose(c);\n+\treturn c;\n+}\n+\n+func main() {\n+\ttest1(XChan(closedsync()));\n+\ttest1(SChan(closedsync()));\n+\n+\ttestasync1(XChan(closedasync()));\n+\ttestasync1(SChan(closedasync()));\n+\ttestasync2(XChan(closedasync()));\n+\ttestasync2(SChan(closedasync()));\n+}\n```