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

[インデックス 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 関数内のロジックの修正です。

  1. 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)
    };
    
  2. allocsg および freesg 関数での isfree の利用:

    • allocsg 関数(SudoG インスタンスを割り当てる関数)では、新しく割り当てられた SudoGisfree0 に初期化します。
    • freesg 関数(SudoG インスタンスをフリーリストに戻す関数)では、isfree が既に 1 であれば "chan.freesg: already free" というパニックを発生させ、二重解放を防ぎます。その後、isfree1 に設定します。
    // allocsg
    \tsg->offset = 0;
    +\tsg->isfree = 0;
    
    // freesg
    +\tif(sg->isfree)
    +\t\tthrow("chan.freesg: already free");
    +\tsg->isfree = 1;
    
  3. gp->param = sg; の削除:

    • sendchan および recvchan 関数内で、ゴルーチン gpparam フィールドに SudoG インスタンス sg を設定する行が削除されました。
    • 同様に、sys·selectgo 関数内の非同期チャネル処理 (asynr, asyns ラベルの箇所) でも gp->param = sg; が削除されています。
    • これは、ゴルーチンが待機状態から復帰する際に、g->param を介して SudoG 情報を渡すのではなく、別のメカニズム(おそらく selectgo 内で直接 sg を処理するか、g->paramnil に設定して再評価を促す)に切り替わったことを示唆しています。
  4. sys·selectgo 関数内のロジック変更:

    • sys·selectgoselect ステートメントの主要な実装です。この関数内で、ゴルーチンがチャネル操作を待機し、再開される際のロジックが大幅に変更されました。
    • g->param = nil; の追加: ゴルーチンが待機状態に入る前に、g->paramnil に設定します。これは、以前の select 実行からの古い SudoG 参照をクリアするためと考えられます。
    • goto loop; の導入と条件分岐の変更:
      • ゴルーチンが sys·Gosched() によってスケジューリングされ、再度実行された後、g->paramnil であれば 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}
    
  5. デバッグ出力の調整:

    • 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 ステートメントがバッファ付きチャネルからの読み取りを処理する際の、ゴルーチンの状態管理とチャネル操作の再評価メカニズムの改善にあります。

  1. SudoGisfree フラグ:

    • SudoG 構造体に isfree フラグが追加されたことで、SudoG インスタンスが現在使用中であるか、またはフリーリストに戻されているかを明確に追跡できるようになりました。
    • allocsgisfree = 0 に初期化し、freesgisfree = 1 に設定することで、SudoG のライフサイクルが厳密に管理されます。
    • freesg での二重解放チェック (if(sg->isfree) throw(...)) は、メモリ破損や不正な状態遷移を防ぐための重要な安全策です。これにより、SudoG インスタンスが誤って複数回フリーリストに戻されたり、まだ使用中のインスタンスが再利用されたりするのを防ぎます。
  2. gp->param = sg; の削除と g->param = nil; の追加:

    • 以前は、チャネル操作が完了してゴルーチンが再開される際に、SudoG インスタンスが gp->param を介してゴルーチンに渡されていました。しかし、この方法では、非同期チャネル操作が完了した後に select が再評価されるべき状況で、古い SudoG 情報が残ってしまう可能性がありました。
    • g->param = nil;sys·selectgo の待機ループに入る直前に追加することで、ゴルーチンが待機状態に入る前に param を明示的にクリアします。これにより、ゴルーチンが再開された際に、古い SudoG 情報を参照するリスクがなくなります。
  3. sys·selectgo 内の goto loop; とバッファ付きチャネルの処理:

    • この変更がバグ修正の最も重要な部分です。ゴルーチンが sys·Gosched() によってスケジューリングされ、再度実行された後、g->paramnil であれば loop ラベルにジャンプして select の処理を最初からやり直すようになりました。
    • さらに、バッファ付きチャネル (c->dataqsiz > 0) の場合、以前は非同期処理を継続するために asyns または asynr にジャンプしていましたが、この変更後は無条件に goto loop; にジャンプするようになりました。
    • このロジック変更の意図は、バッファ付きチャネルの非同期操作が完了した場合でも、select が即座に結果を処理するのではなく、一度 select の評価プロセス全体を再開させることにあります。これにより、チャネルの状態が完全に更新され、select が最新のチャネル操作の準備状況を正しく判断できるようになります。
    • 以前のバグは、非同期チャネル操作が完了した際に、select がその完了を正しく認識せず、既に読み取られた値を再度「準備完了」と誤認して複数回返してしまうことに起因していたと考えられます。goto loop; による再評価は、この競合状態を解消し、select が常にチャネルの真の状態に基づいて動作することを保証します。

これらの変更により、Goランタイムは select ステートメントとバッファ付きチャネルの非同期操作をより堅牢に処理できるようになり、同じ値が複数回返されるという深刻なバグが修正されました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード: src/runtime/chan.c
  • Go言語のチャネルと select の内部実装に関する一般的な情報源 (Goのブログ記事や技術解説記事など)
  • BuganizerはGoogle内部のバグトラッキングシステムであり、外部からはアクセスできません。