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

[インデックス 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 のセマンティクスをより正確に反映させるための変更を加えています。主な技術的変更点は以下の通りです。

  1. 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 であるという予期せぬ状態が発生した場合に、チャネルの状態を再評価させるためのものです。これは、チャネルがブロック中にクローズされた場合などに発生しうる競合状態を適切に処理するために重要です。
  2. pres (present) フラグの正しい設定:

    • sendchan および chanrecv 関数において、チャネルがクローズされた場合の pres (present) フラグの値を false から true に変更しています。
    • Go言語では、value, ok := <-ch のようにチャネルから受信する際に ok 変数を使用できます。チャネルがクローズされ、かつバッファが空になった場合、okfalse になりますが、それ以外の場合(クローズされたチャネルからゼロ値を受信する場合や、クローズされたチャネルに送信する場合)は true になるべきです。
    • この修正により、ok のセマンティクスがGo言語の仕様と一致し、プログラマがチャネルのクローズ状態を正確に判断できるようになります。特に、クローズされたチャネルからの受信でゼロ値が返される場合でも、その操作自体は「成功」したと見なされるため、oktrue であるべきです。
  3. select ステートメントにおけるクローズチャネル処理の改善:

    • select ステートメントの内部ロジック (runtime/chan.cselect 関連コード) において、クローズされたチャネルの処理がより明確かつ正確になるようにリファクタリングされています。
    • 以前は、c->closed & Wclosed のチェック後に直接 gots (送信成功) や gotr (受信成功) にジャンプしていましたが、これはクローズされたチャネルに対する操作のセマンティクスを正確に反映していませんでした。
    • 新しいコードでは、sclose (送信クローズ) と rclose (受信クローズ) という新しい goto ラベルが導入されています。
      • sclose は、クローズされたチャネルへの送信ケースで呼び出され、エラーカウンタをインクリメントし、retc (リターン) にジャンプします。これは、クローズされたチャネルへの送信がパニックを引き起こすというGoの仕様に沿ったものです。
      • rclose は、クローズされたチャネルからの受信ケースで呼び出され、受信バッファにゼロ値をコピーし、Rclosed フラグを設定し、エラーカウンタをインクリメントして retc にジャンプします。これにより、クローズされたチャネルからの受信がゼロ値を返すというセマンティクスが保証されます。
    • この変更により、select がクローズされたチャネルを検出した際の挙動が、Go言語のチャネル仕様に厳密に準拠するようになりました。

これらの変更は、Goランタイムのチャネル実装の低レベルな部分に深く関わっており、Goの並行処理モデルの正確性と信頼性を向上させる上で非常に重要です。

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

このコミットにおけるコアとなるコードの変更箇所は、主に src/runtime/chan.ctest/closedchan.go に集中しています。

src/runtime/chan.c の変更点

  1. 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 (バッファ付きチャネルのチェック) の前に移動されました。
  2. 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` に戻るロジックが追加されました。
    
    
  3. 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; に変更されました。
  4. 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 の変更の意図

  1. closed チェックの早期化と loop ラベル:

    • sendchan において c->closed & Wclosed のチェックを c->dataqsiz > 0 の前に移動し、loop ラベルを導入したことで、チャネルが書き込み用にクローズされている場合、バッファの状態に関わらず、送信操作が即座に「クローズされたチャネルへの送信」というエラーパス(パニックを引き起こす)に進むことが保証されます。これはGoのチャネルの仕様に厳密に準拠するための修正です。以前の順序では、バッファ付きチャネルの場合に誤ったパスに進む可能性がありました。
    • g->param == nil の場合に loop に戻るロジックは、ゴルーチンがブロック解除された際に、何らかの理由で sg (sudog) が有効でない場合に、チャネルの状態を再評価させるための安全策です。これは、チャネルがブロック中にクローズされるといった複雑な競合条件下で、ランタイムが正しい状態遷移を行うために重要です。
  2. pres フラグの修正:

    • *pres = false から *pres = true への変更は、Go言語の value, ok := <-chch <- value, ok のセマンティクスを正確に反映するためのものです。Goでは、チャネルがクローズされていても、受信操作がゼロ値を返す場合や、送信操作がパニックを引き起こす場合でも、その操作自体は「完了した」と見なされ、oktrue になるべきです。okfalse になるのは、チャネルがクローズされ、かつバッファが空になった後に受信しようとした場合のみです。この修正により、Goのチャネルの振る舞いがより直感的で予測可能になります。
  3. select ロジックのリファクタリング (rclose, sclose):

    • select ステートメント内でのクローズされたチャネルの処理を rclosesclose という専用の goto ラベルに集約したことで、コードの可読性と保守性が向上しました。
    • rclose では、クローズされたチャネルからの受信時にゼロ値をコピーし、Rclosed フラグを設定することで、Goの仕様通りの挙動を保証します。
    • sclose では、クローズされたチャネルへの送信時にエラーカウンタをインクリメントし、パニックを引き起こす準備をします。
    • これらの変更により、select が複数のチャネル操作を同時に扱う際に、クローズされたチャネルがどのように優先的に処理され、どのような結果を返すかが明確になります。これは、Goの並行処理における重要な側面であり、デッドロックや予期せぬパニックを防ぐ上で不可欠です。

test/closedchan.go の追加の意図

  • この新しいテストファイルは、closeclosed の操作に関する既存のバグを特定し、修正された挙動を検証するために不可欠です。
  • XChanSChan の両方でテストを行うことで、直接的なチャネル操作と select を介したチャネル操作の両方で、closeclosed のセマンティクスが正しく機能することを確認しています。
  • 同期チャネルと非同期チャネルの両方でテストを行うことで、バッファリングの有無がチャネルのクローズ挙動に与える影響を網羅的に検証しています。特に、非同期チャネルがクローズされた後もバッファ内の値が消費されるべきであるという重要なセマンティクスがテストされています。
  • 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 use google_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言語の並行処理モデルの基盤であるチャネルの堅牢性と予測可能性を向上させることを目的としています。具体的には、以下の点が背景として考えられます。

  1. closeclosed のセマンティクスの明確化と統一: チャネルがクローズされた際に、送信操作がパニックを起こすこと、受信操作がゼロ値を返すこと、そして ok 値が true になることなど、Go言語の仕様で定められた振る舞いをランタイムが正確に反映していることを保証する必要がありました。
  2. バッファ付きチャネルの挙動の修正: バッファ付きチャネルがクローズされた場合、バッファに残っている値は引き続き受信可能であるべきです。しかし、当時の実装では、この点が正しく処理されていないケースがあった可能性があります。
  3. select ステートメントとの連携: select ステートメントは複数のチャネル操作を同時に待機する強力な機能ですが、クローズされたチャネルが select のケースに含まれる場合の挙動が曖昧であったり、誤っていたりする可能性がありました。
  4. テストカバレッジの不足: closeclosed の複雑な相互作用を網羅するテストが不足していたため、潜在的なバグが見過ごされていた可能性があります。このコミットで追加された test/closedchan.go は、これらのエッジケースを体系的に検証するために導入されました。

これらの背景から、このコミットはGoランタイムのチャネル実装の安定性と正確性を高める上で不可欠なものでした。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の概念とランタイムの内部構造に関する知識が必要です。

Go言語のチャネル (Channels)

Go言語におけるチャネルは、ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送受信できます。

  • チャネルの作成: make(chan int) (バッファなしチャネル) または make(chan int, 10) (バッファ付きチャネル) のように作成します。
  • 送信: ch <- value のようにチャネルに値を送信します。
  • 受信: value := <-ch または value, ok := <-ch のようにチャネルから値を受信します。
    • ok 変数は、受信が成功したかどうか(チャネルが閉じられていないか、またはバッファに値が残っていたか)を示します。チャネルが閉じられ、かつバッファが空の場合、okfalse になり、value はその型のゼロ値になります。
  • チャネルのクローズ: close(ch) のようにチャネルをクローズします。
    • クローズされたチャネルへの送信はパニックを引き起こします。
    • クローズされたチャネルからの受信は、バッファに値が残っていればその値を返し、バッファが空になればその型のゼロ値を返します。この際、okfalse になります。
  • 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 のセマンティクスをより正確に反映させるための変更を加えています。主な技術的変更点は以下の通りです。

  1. 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 であるという予期せぬ状態が発生した場合に、チャネルの状態を再評価させるためのものです。これは、チャネルがブロック中にクローズされた場合などに発生しうる競合状態を適切に処理するために重要です。
  2. pres (present) フラグの正しい設定:

    • sendchan および chanrecv 関数において、チャネルがクローズされた場合の pres (present) フラグの値を false から true に変更しています。
    • Go言語では、value, ok := <-ch のようにチャネルから受信する際に ok 変数を使用できます。チャネルがクローズされ、かつバッファが空になった場合、okfalse になりますが、それ以外の場合(クローズされたチャネルからゼロ値を受信する場合や、クローズされたチャネルに送信する場合)は true になるべきです。
    • この修正により、ok のセマンティクスがGo言語の仕様と一致し、プログラマがチャネルのクローズ状態を正確に判断できるようになります。特に、クローズされたチャネルからの受信でゼロ値が返される場合でも、その操作自体は「成功」したと見なされるため、oktrue であるべきです。
  3. select ステートメントにおけるクローズチャネル処理の改善:

    • select ステートメントの内部ロジック (runtime/chan.cselect 関連コード) において、クローズされたチャネルの処理がより明確かつ正確になるようにリファクタリングされています。
    • 以前は、c->closed & Wclosed のチェック後に直接 gots (送信成功) や gotr (受信成功) にジャンプしていましたが、これはクローズされたチャネルに対する操作のセマンティクスを正確に反映していませんでした。
    • 新しいコードでは、sclose (送信クローズ) と rclose (受信クローズ) という新しい goto ラベルが導入されています。
      • sclose は、クローズされたチャネルへの送信ケースで呼び出され、エラーカウンタをインクリメントし、retc (リターン) にジャンプします。これは、クローズされたチャネルへの送信がパニックを引き起こすというGoの仕様に沿ったものです。
      • rclose は、クローズされたチャネルからの受信ケースで呼び出され、受信バッファにゼロ値をコピーし、Rclosed フラグを設定し、エラーカウンタをインクリメントして retc にジャンプします。これにより、クローズされたチャネルからの受信がゼロ値を返すというセマンティクスが保証されます。
    • この変更により、select がクローズされたチャネルを検出した際の挙動が、Go言語のチャネル仕様に厳密に準拠するようになりました。

これらの変更は、Goランタイムのチャネル実装の低レベルな部分に深く関わっており、Goの並行処理モデルの正確性と信頼性を向上させる上で非常に重要です。

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

このコミットにおけるコアとなるコードの変更箇所は、主に src/runtime/chan.ctest/closedchan.go に集中しています。

src/runtime/chan.c の変更点

  1. 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 (バッファ付きチャネルのチェック) の前に移動されました。
  2. 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` に戻るロジックが追加されました。
    
    
  3. 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; に変更されました。
  4. 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言語の並行処理モデルの基盤であるチャネルの堅牢性と予測可能性を向上させることを目的としています。具体的には、以下の点が背景として考えられます。

  1. closeclosed のセマンティクスの明確化と統一: チャネルがクローズされた際に、送信操作がパニックを起こすこと、受信操作がゼロ値を返すこと、そして ok 値が true になることなど、Go言語の仕様で定められた振る舞いをランタイムが正確に反映していることを保証する必要がありました。
  2. バッファ付きチャネルの挙動の修正: バッファ付きチャネルがクローズされた場合、バッファに残っている値は引き続き受信可能であるべきです。しかし、当時の実装では、この点が正しく処理されていないケースがあった可能性があります。
  3. select ステートメントとの連携: select ステートメントは複数のチャネル操作を同時に待機する強力な機能ですが、クローズされたチャネルが select のケースに含まれる場合の挙動が曖昧であったり、誤っていたりする可能性がありました。
  4. テストカバレッジの不足: closeclosed の複雑な相互作用を網羅するテストが不足していたため、潜在的なバグが見過ごされていた可能性があります。このコミットで追加された test/closedchan.go は、これらのエッジケースを体系的に検証するために導入されました。

これらの背景から、このコミットはGoランタイムのチャネル実装の安定性と正確性を高める上で不可欠なものでした。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の概念とランタイムの内部構造に関する知識が必要です。

Go言語のチャネル (Channels)

Go言語におけるチャネルは、ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送受信できます。

  • チャネルの作成: make(chan int) (バッファなしチャネル) または make(chan int, 10) (バッファ付きチャネル) のように作成します。
  • 送信: ch <- value のようにチャネルに値を送信します。
  • 受信: value := <-ch または value, ok := <-ch のようにチャネルから値を受信します。
    • ok 変数は、受信が成功したかどうか(チャネルが閉じられていないか、またはバッファに値が残っていたか)を示します。チャネルが閉じられ、かつバッファが空の場合、okfalse になり、value はその型のゼロ値になります。
  • チャネルのクローズ: close(ch) のようにチャネルをクローズします。
    • クローズされたチャネルへの送信はパニックを引き起こします。
    • クローズされたチャネルからの受信は、バッファに値が残っていればその値を返し、バッファが空になればその型のゼロ値を返します。この際、okfalse になります。
  • 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 のセマンティクスをより正確に反映させるための変更を加えています。主な技術的変更点は以下の通りです。

  1. 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 であるという予期せぬ状態が発生した場合に、チャネルの状態を再評価させるためのものです。これは、チャネルがブロック中にクローズされた場合などに発生しうる競合状態を適切に処理するために重要です。
  2. pres (present) フラグの正しい設定:

    • sendchan および chanrecv 関数において、チャネルがクローズされた場合の pres (present) フラグの値を false から true に変更しています。
    • Go言語では、value, ok := <-ch のようにチャネルから受信する際に ok 変数を使用できます。チャネルがクローズされ、かつバッファが空になった場合、okfalse になりますが、それ以外の場合(クローズされたチャネルからゼロ値を受信する場合や、クローズされたチャネルに送信する場合)は true になるべきです。
    • この修正により、ok のセマンティクスがGo言語の仕様と一致し、プログラマがチャネルのクローズ状態を正確に判断できるようになります。特に、クローズされたチャネルからの受信でゼロ値が返される場合でも、その操作自体は「成功」したと見なされるため、oktrue であるべきです。
  3. select ステートメントにおけるクローズチャネル処理の改善:

    • select ステートメントの内部ロジック (runtime/chan.cselect 関連コード) において、クローズされたチャネルの処理がより明確かつ正確になるようにリファクタリングされています。
    • 以前は、c->closed & Wclosed のチェック後に直接 gots (送信成功) や gotr (受信成功) にジャンプしていましたが、これはクローズされたチャネルに対する操作のセマンティクスを正確に反映していませんでした。
    • 新しいコードでは、sclose (送信クローズ) と rclose (受信クローズ) という新しい goto ラベルが導入されています。
      • sclose は、クローズされたチャネルへの送信ケースで呼び出され、エラーカウンタをインクリメントし、retc (リターン) にジャンプします。これは、クローズされたチャネルへの送信がパニックを引き起こすというGoの仕様に沿ったものです。
      • rclose は、クローズされたチャネルからの受信ケースで呼び出され、受信バッファにゼロ値をコピーし、Rclosed フラグを設定し、エラーカウンタをインクリメントして retc にジャンプします。これにより、クローズされたチャネルからの受信がゼロ値を返すというセマンティクスが保証されます。
    • この変更により、select がクローズされたチャネルを検出した際の挙動が、Go言語のチャネル仕様に厳密に準拠するようになりました。

これらの変更は、Goランタイムのチャネル実装の低レベルな部分に深く関わっており、Goの並行処理モデルの正確性と信頼性を向上させる上で非常に重要です。

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

このコミットにおけるコアとなるコードの変更箇所は、主に src/runtime/chan.ctest/closedchan.go に集中しています。

src/runtime/chan.c の変更点

  1. 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 (バッファ付きチャネルのチェック) の前に移動されました。
  2. 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` に戻るロジックが追加されました。
    
    
  3. 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; に変更されました。
  4. 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```