[インデックス 1550] ファイルの概要
このコミットは、Goランタイムのチャネル実装に関するバグ修正です。対象ファイルは src/runtime/chan.c
であり、Go言語の並行処理の根幹をなすチャネルの動作、特に select
ステートメントと非同期チャネルの読み取りに関する低レベルな実装を扱っています。このファイルは、チャネルの作成、送受信、および select
操作の内部ロジックを定義しており、Goプログラムの並行性の正確性と効率性に直接影響を与えます。
コミット
commit 3338c71fc6253c4010c804435be770e3b4beb9ee
Author: Ken Thompson <ken@golang.org>
Date: Sat Jan 24 15:58:44 2009 -0800
bug in async select read
buganizer 1589219
channel is returning same values multiple times
R=r
OCL=23447
CL=23447
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3338c71fc6253c4010c804435be770e3b4beb9ee
元コミット内容
非同期 select
読み取りにおけるバグ。
Buganizer 1589219
チャネルが同じ値を複数回返す。
変更の背景
このコミットは、Go言語のチャネルと select
ステートメントの組み合わせにおいて発生していた深刻なバグを修正するために行われました。具体的には、バッファ付きチャネルからの非同期読み取り(select
ステートメント内で case <-ch:
のように使用される場合)において、チャネルが既に読み取られたはずの同じ値を複数回返してしまうという問題が発生していました。これは、並行処理の正確性を著しく損なうものであり、Goプログラムの予測不可能な動作やデータ破損を引き起こす可能性がありました。
コミットメッセージにある "buganizer 1589219" は、Google内部のバグトラッキングシステムにおけるこの問題のIDを示しています。このバグは、Go言語の初期開発段階で発見され、チャネルの低レベルな同期メカニズムにおける競合状態や状態管理の不備に起因していたと考えられます。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の概念とランタイムの内部構造に関する知識が必要です。
Goのチャネル (Channels)
Go言語におけるチャネルは、ゴルーチン間で値を送受信するための主要な通信メカニズムです。チャネルは、Goの並行処理モデルであるCSP (Communicating Sequential Processes) の中心的な要素であり、ゴルーチン間の同期と通信を安全に行うための手段を提供します。
- バッファなしチャネル: 送信側と受信側が同時に準備ができていないとブロックします。同期通信に使用されます。
- バッファ付きチャネル: 指定された数の要素を格納できるバッファを持ちます。バッファが満杯でない限り送信はブロックせず、バッファが空でない限り受信はブロックしません。非同期通信に使用されます。
select
ステートメント
select
ステートメントは、複数のチャネル操作(送受信)を同時に待機し、準備ができた最初の操作を実行するために使用されます。これにより、複数の並行タスクの中から最初に完了したものを処理したり、タイムアウトを設定したりする柔軟な並行処理パターンを実装できます。
select
の内部では、どのチャネル操作が準備できたかを効率的に判断し、対応するゴルーチンをスケジューリングする複雑なロジックがGoランタイムによって実行されます。
Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ゴルーチンのスケジューリング、メモリ管理(ガベージコレクション)、チャネルの実装、システムコールとのインターフェースなどが含まれます。チャネル操作は、Goランタイムの src/runtime/chan.c
ファイルにC言語で実装されており、低レベルなロック、キュー、ゴルーチンの状態遷移を伴います。
SudoG
構造体
src/runtime/chan.c
内で定義されている SudoG
(Pseudo Goroutine) 構造体は、select
操作においてチャネル操作を待機しているゴルーチンの情報を保持するために使用されます。この構造体は、待機中のゴルーチン (g
)、select
ステートメントの世代番号 (selgen
)、および select
ケースのオフセット (offset
) などの情報を含みます。チャネルの送受信キューにこの SudoG
インスタンスが追加され、対応するチャネル操作が準備できた際に、この情報を用いてゴルーチンが再開されます。
非同期チャネル操作
バッファ付きチャネルにおける送受信は非同期的に行われます。つまり、送信側はバッファに空きがあればすぐに値を書き込み、受信側はバッファに値があればすぐに読み取ることができます。この非同期性が、複数のゴルーチンが同時にチャネルにアクセスする際の競合状態や、select
ステートメントが複数の非同期操作を処理する際の複雑さを生み出す原因となります。
技術的詳細
このコミットの主要な変更点は、src/runtime/chan.c
における SudoG
構造体の変更と、selectgo
関数内のロジックの修正です。
-
SudoG
構造体へのisfree
フィールドの追加:SudoG
構造体にint8 isfree;
フィールドが追加されました。これは、SudoG
インスタンスが現在使用中であるか、またはフリーリストに戻されているかを示すフラグです。- 以前は
offset
フィールドの後にselgen
が来ていましたが、isfree
の追加により順序が変更されています。
struct SudoG { G*\tg;\t\t// g and selgen constitute -\tint16\toffset;\t\t// offset of case number \tint32\tselgen;\t\t// a weak pointer to g +\tint32\tselgen;\t\t// a weak pointer to g +\tint16\toffset;\t\t// offset of case number +\tint8\tisfree;\t\t// offset of case number \tSudoG*\tlink; \tbyte\telem[8];\t// synch data element (+ more) };
-
allocsg
およびfreesg
関数でのisfree
の利用:allocsg
関数(SudoG
インスタンスを割り当てる関数)では、新しく割り当てられたSudoG
のisfree
を0
に初期化します。freesg
関数(SudoG
インスタンスをフリーリストに戻す関数)では、isfree
が既に1
であれば "chan.freesg: already free" というパニックを発生させ、二重解放を防ぎます。その後、isfree
を1
に設定します。
// allocsg \tsg->offset = 0; +\tsg->isfree = 0; // freesg +\tif(sg->isfree) +\t\tthrow("chan.freesg: already free"); +\tsg->isfree = 1;
-
gp->param = sg;
の削除:sendchan
およびrecvchan
関数内で、ゴルーチンgp
のparam
フィールドにSudoG
インスタンスsg
を設定する行が削除されました。- 同様に、
sys·selectgo
関数内の非同期チャネル処理 (asynr
,asyns
ラベルの箇所) でもgp->param = sg;
が削除されています。 - これは、ゴルーチンが待機状態から復帰する際に、
g->param
を介してSudoG
情報を渡すのではなく、別のメカニズム(おそらくselectgo
内で直接sg
を処理するか、g->param
をnil
に設定して再評価を促す)に切り替わったことを示唆しています。
-
sys·selectgo
関数内のロジック変更:sys·selectgo
はselect
ステートメントの主要な実装です。この関数内で、ゴルーチンがチャネル操作を待機し、再開される際のロジックが大幅に変更されました。g->param = nil;
の追加: ゴルーチンが待機状態に入る前に、g->param
をnil
に設定します。これは、以前のselect
実行からの古いSudoG
参照をクリアするためと考えられます。goto loop;
の導入と条件分岐の変更:- ゴルーチンが
sys·Gosched()
によってスケジューリングされ、再度実行された後、g->param
がnil
であればloop
ラベルにジャンプしてselect
の処理を最初からやり直すようになりました。これは、select
が再評価されるべき状況(例えば、別のゴルーチンがチャネル操作を完了したが、その結果がまだこのselect
に反映されていない場合)に対応するためと考えられます。 - バッファ付きチャネル (
c->dataqsiz > 0
) の場合、以前はasyns
またはasynr
ラベルにジャンプして非同期処理を継続していましたが、この変更後は無条件にgoto loop;
にジャンプするようになりました。これは、バッファ付きチャネルの非同期操作が完了した場合でも、select
が再度全体の状態を評価し、適切なケースを選択し直すことを強制することで、同じ値が複数回返されるバグを防ぐ意図があると考えられます。
- ゴルーチンが
\tg->param = nil; \tg->status = Gwaiting; \tunlock(&chanlock); \tsys·Gosched(); \tlock(&chanlock); \tsg = g->param; +\tif(sg == nil) +\t\tgoto loop; +\ \to = sg->offset; \tcas = sel->scase[o]; \tc = cas->chan; +\tif(c->dataqsiz > 0) { +//\t\tprints("shouldnt happen\\n"); +\t\tgoto loop; +\t}
-
デバッグ出力の調整:
prints
関数の引数に文字列リテラルを直接渡す形式に変更され、より具体的なデバッグメッセージが出力されるようになりました。例えば、"newselect s="
が"selectsend s="
や"selectrecv s="
などに変わっています。これは機能的な変更ではなく、デバッグの利便性向上のためのものです。
これらの変更は、select
ステートメントがバッファ付きチャネルを扱う際の競合状態を解消し、SudoG
インスタンスのライフサイクル管理をより厳密にすることで、チャネルが同じ値を複数回返すというバグを修正することを目的としています。特に、g->param
のクリアと goto loop;
による select
の再評価は、非同期操作の完了後に select
が最新のチャネル状態を正しく反映するための重要なメカニズムです。
コアとなるコードの変更箇所
--- a/src/runtime/chan.c
+++ b/src/runtime/chan.c
@@ -5,6 +5,7 @@
#include "runtime.h"
static int32 debug = 0;
+static int32 xxx = 0;
static Lock chanlock;
typedef struct Hchan Hchan;
@@ -17,8 +18,9 @@ typedef struct Scase Scase;
struct SudoG
{
G*\tg;\t\t// g and selgen constitute
-\tint16\toffset;\t\t// offset of case number
\tint32\tselgen;\t\t// a weak pointer to g
+\tint16\toffset;\t\t// offset of case number
+\tint8\tisfree;\t\t// offset of case number
SudoG*\tlink;
byte\telem[8];\t// synch data element (+ more)
};
@@ -206,7 +208,6 @@ sendchan(Hchan *c, byte *ep, bool *pres)\n return;\n \n asynch:\
-//prints("
asend
");\n while(c->qcount >= c->dataqsiz) {\
\tif(pres != nil) {\
\t\tunlock(&chanlock);\
@@ -229,10 +230,8 @@ asynch:\
sg = dequeue(&c->recvq, c);\
if(sg != nil) {\
\tgp = sg->g;\
-\t\tgp->param = sg;\
\tfreesg(c, sg);\
\tunlock(&chanlock);\
-//prints("wakeup
");\n \tready(gp);\
} else\
\tunlock(&chanlock);\
@@ -312,7 +311,6 @@ asynch:\
sg = dequeue(&c->sendq, c);\
if(sg != nil) {\
\tgp = sg->g;\
-\t\tgp->param = sg;\
\tfreesg(c, sg);\
\tunlock(&chanlock);\
\tready(gp);\
@@ -411,7 +409,7 @@ sys·newselect(int32 size, Select *sel)\
if(debug) {\
\tprints("newselect s=");\
\tsys·printpointer(sel);\
-\t\tprints("newselect size=");\
+\t\tprints(" size=");\
\tsys·printint(size);\
\tprints("
");\n }\
@@ -451,7 +450,7 @@ sys·selectsend(Select *sel, Hchan *c, ...)\
c->elemalg->copy(c->elemsize, cas->u.elem, ae);\
if(debug) {\
-\t\tprints("newselect s=");\
+\t\tprints("selectsend s=");\
\tsys·printpointer(sel);\
\tprints(" pc=");\
\tsys·printpointer(cas->pc);\
@@ -495,7 +494,7 @@ sys·selectrecv(Select *sel, Hchan *c, ...)\
cas->u.elemp = *(byte**)((byte*)&sel + eo);\
if(debug) {\
-\t\tprints("newselect s=");\
+\t\tprints("selectrecv s=");\
\tsys·printpointer(sel);\
\tprints(" pc=");\
\tsys·printpointer(cas->pc);\
@@ -510,7 +509,7 @@ sys·selectrecv(Select *sel, Hchan *c, ...)\
}\
\
\
-// selectrecv(sel *byte) (selected bool);\
+// selectdefaul(sel *byte) (selected bool);\
void\
sys·selectdefault(Select *sel, ...)\
{\
@@ -534,7 +533,7 @@ sys·selectdefault(Select *sel, ...)\
cas->u.elemp = nil;\
if(debug) {\
-\t\tprints("newselect s=");\
+\t\tprints("selectdefault s=");\
\tsys·printpointer(sel);\
\tprints(" pc=");\
\tsys·printpointer(cas->pc);\
@@ -546,7 +545,6 @@ sys·selectdefault(Select *sel, ...)\
}\
}\
\
-uint32\txxx\t= 0;\
\
// selectgo(sel *byte);\
void\
@@ -589,6 +587,7 @@ sys·selectgo(Select *sel)\
\n\tlock(&chanlock);\
\n+loop:\
\t// pass 1 - look for something already waiting\
\tdfl = nil;\
\tfor(i=0; i<sel->ncase; i++) {\
@@ -688,16 +687,25 @@ sys·selectgo(Select *sel)\
\t\to -= sel->ncase;\
\t}\
\n+\tg->param = nil;\
\tg->status = Gwaiting;\
\tunlock(&chanlock);\
\tsys·Gosched();\
\n \tlock(&chanlock);\
\tsg = g->param;\
+\tif(sg == nil)\
+\t\tgoto loop;\
+\n \to = sg->offset;\
\tcas = sel->scase[o];\
\tc = cas->chan;\
\n+\tif(c->dataqsiz > 0) {\
+//\t\tprints("shouldnt happen
");\n+\t\tgoto loop;\
+\t}\
+\n \tif(xxx) {\
\t\tprints("wait-return: sel=");\
\t\tsys·printpointer(sel);\
@@ -712,12 +720,6 @@ sys·selectgo(Select *sel)\
\t\tprints("
");\n \t}\
\n-\tif(c->dataqsiz > 0) {\
-\t\tif(cas->send)\
-\t\t\tgoto asyns;\
-\t\tgoto asynr;\
-\t}\
-\n \tif(!cas->send) {\
\t\tif(cas->u.elemp != nil)\
\t\t\tc->elemalg->copy(c->elemsize, cas->u.elemp, sg->elem);\
@@ -734,7 +736,6 @@ asynr:\
\tsg = dequeue(&c->sendq, c);\
\tif(sg != nil) {\
\t\tgp = sg->g;\
-\t\tgp->param = sg;\
\t\tfreesg(c, sg);\
\t\tready(gp);\
\t}\
@@ -748,7 +749,6 @@ asyns:\
\tsg = dequeue(&c->recvq, c);\
\tif(sg != nil) {\
\t\tgp = sg->g;\
-\t\tgp->param = sg;\
\t\tfreesg(c, sg);\
\t\tready(gp);\
\t}\
@@ -849,6 +849,7 @@ allocsg(Hchan *c)\
\tsg->selgen = g->selgen;\
\tsg->g = g;\
\tsg->offset = 0;\
+\tsg->isfree = 0;\
\n \treturn sg;\
}\
\n@@ -856,6 +857,9 @@ allocsg(Hchan *c)\
static void\
freesg(Hchan *c, SudoG *sg)\
{\
+\tif(sg->isfree)\
+\t\tthrow("chan.freesg: already free");
+\tsg->isfree = 1;\
\tsg->link = c->free;\
\tc->free = sg;\
}\
コアとなるコードの解説
このコミットの核心は、select
ステートメントがバッファ付きチャネルからの読み取りを処理する際の、ゴルーチンの状態管理とチャネル操作の再評価メカニズムの改善にあります。
-
SudoG
のisfree
フラグ:SudoG
構造体にisfree
フラグが追加されたことで、SudoG
インスタンスが現在使用中であるか、またはフリーリストに戻されているかを明確に追跡できるようになりました。allocsg
でisfree = 0
に初期化し、freesg
でisfree = 1
に設定することで、SudoG
のライフサイクルが厳密に管理されます。freesg
での二重解放チェック (if(sg->isfree) throw(...)
) は、メモリ破損や不正な状態遷移を防ぐための重要な安全策です。これにより、SudoG
インスタンスが誤って複数回フリーリストに戻されたり、まだ使用中のインスタンスが再利用されたりするのを防ぎます。
-
gp->param = sg;
の削除とg->param = nil;
の追加:- 以前は、チャネル操作が完了してゴルーチンが再開される際に、
SudoG
インスタンスがgp->param
を介してゴルーチンに渡されていました。しかし、この方法では、非同期チャネル操作が完了した後にselect
が再評価されるべき状況で、古いSudoG
情報が残ってしまう可能性がありました。 g->param = nil;
をsys·selectgo
の待機ループに入る直前に追加することで、ゴルーチンが待機状態に入る前にparam
を明示的にクリアします。これにより、ゴルーチンが再開された際に、古いSudoG
情報を参照するリスクがなくなります。
- 以前は、チャネル操作が完了してゴルーチンが再開される際に、
-
sys·selectgo
内のgoto loop;
とバッファ付きチャネルの処理:- この変更がバグ修正の最も重要な部分です。ゴルーチンが
sys·Gosched()
によってスケジューリングされ、再度実行された後、g->param
がnil
であればloop
ラベルにジャンプしてselect
の処理を最初からやり直すようになりました。 - さらに、バッファ付きチャネル (
c->dataqsiz > 0
) の場合、以前は非同期処理を継続するためにasyns
またはasynr
にジャンプしていましたが、この変更後は無条件にgoto loop;
にジャンプするようになりました。 - このロジック変更の意図は、バッファ付きチャネルの非同期操作が完了した場合でも、
select
が即座に結果を処理するのではなく、一度select
の評価プロセス全体を再開させることにあります。これにより、チャネルの状態が完全に更新され、select
が最新のチャネル操作の準備状況を正しく判断できるようになります。 - 以前のバグは、非同期チャネル操作が完了した際に、
select
がその完了を正しく認識せず、既に読み取られた値を再度「準備完了」と誤認して複数回返してしまうことに起因していたと考えられます。goto loop;
による再評価は、この競合状態を解消し、select
が常にチャネルの真の状態に基づいて動作することを保証します。
- この変更がバグ修正の最も重要な部分です。ゴルーチンが
これらの変更により、Goランタイムは select
ステートメントとバッファ付きチャネルの非同期操作をより堅牢に処理できるようになり、同じ値が複数回返されるという深刻なバグが修正されました。
関連リンク
- Go言語のチャネルに関する公式ドキュメント: https://go.dev/tour/concurrency/2
- Go言語の
select
ステートメントに関する公式ドキュメント: https://go.dev/tour/concurrency/5
参考にした情報源リンク
- Go言語のソースコード:
src/runtime/chan.c
- Go言語のチャネルと
select
の内部実装に関する一般的な情報源 (Goのブログ記事や技術解説記事など)- Goのチャネルの設計思想: https://go.dev/blog/pipelines
- Goの並行処理の基礎: https://go.dev/blog/concurrency-is-not-parallelism
- BuganizerはGoogle内部のバグトラッキングシステムであり、外部からはアクセスできません。