[インデックス 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内部のバグトラッキングシステムであり、外部からはアクセスできません。