[インデックス 1085] ファイルの概要
このコミットは、Go言語のランタイムにおけるselect
ステートメントのデフォルトケースに関するバグ修正を扱っています。具体的には、src/runtime/chan.c
ファイル内のチャネル操作に関連するロジックが変更されています。
コミット
commit 9b827cf9a040b0e9b1bf20169277edec2ce5407d
Author: Ken Thompson <ken@golang.org>
Date: Thu Nov 6 17:50:28 2008 -0800
bug in select default
R=r
OCL=18741
CL=18741
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9b827cf9a040b0e9b1bf20169277edec2ce5407d
元コミット内容
bug in select default
このコミットメッセージは非常に簡潔ですが、Go言語のselect
ステートメントにおけるデフォルトケースの振る舞いにバグが存在し、それを修正したことを示しています。R=r
、OCL=18741
、CL=18741
は、当時のGoプロジェクトのコードレビューシステムや変更リスト番号に関連するメタデータです。
変更の背景
Go言語のselect
ステートメントは、複数のチャネル操作を待機し、準備ができた最初の操作を実行するための強力なプリミティブです。select
にはオプションでdefault
ケースを含めることができ、これはどのチャネル操作もすぐに実行できない場合に実行されます。
このコミットが行われた2008年11月は、Go言語がまだ初期開発段階にあった時期です。ランタイムのチャネル実装は、並行処理の基盤として非常に重要であり、その正確な動作は言語の信頼性に直結します。
当時のselect
のデフォルトケースの実装には、特定の条件下で意図しない動作を引き起こすバグが存在したと考えられます。このバグは、select
がチャネル操作を評価する際のロジック、特に非同期チャネル(バッファ付きチャネル)と同期チャネル(バッファなしチャネル)の振る舞い、そしてdefault
ケースへのフォールバックの順序に関連していた可能性があります。
Ken Thompson氏によるこの修正は、Go言語の並行処理モデルの健全性を確保するための重要なステップでした。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とC言語の知識が必要です。
-
Go言語のチャネル (Channels):
- Goにおけるチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。
- バッファなしチャネル: 送信操作は受信操作が準備できるまでブロックし、受信操作は送信操作が準備できるまでブロックします。同期的な通信に使用されます。
- バッファ付きチャネル: 指定された数の値を格納できるキューを持つチャネルです。バッファが満杯になるまで送信はブロックせず、バッファが空になるまで受信はブロックしません。非同期的な通信に使用されます。
- チャネルは、Goの並行処理モデルであるCSP (Communicating Sequential Processes) の中心的な要素です。
-
Go言語の
select
ステートメント:select
は、複数のチャネル操作(送信または受信)の中から、準備ができた最初の操作を実行するために使用されます。case
句にはチャネル操作が記述されます。default
句はオプションで、どのcase
もすぐに実行できない場合に実行されます。default
句が存在する場合、select
はブロックしません。select
の評価順序はランダムであり、複数のcase
が準備できている場合、Goランタイムがどれか一つを非決定的に選択します。
-
Goランタイム (Go Runtime):
- Goプログラムは、Goランタイム上で動作します。ランタイムは、ゴルーチンのスケジューリング、チャネルの管理、ガベージコレクションなど、低レベルの操作を処理します。
- このコミットで変更されている
src/runtime/chan.c
は、Goランタイムの一部であり、チャネルの基本的な操作(作成、送受信、select
の処理)をC言語で実装しています。
-
C言語の
goto
ステートメント:- C言語における
goto
は、プログラムの実行フローをラベル付けされた任意の場所にジャンプさせるために使用されます。 - 現代のプログラミングでは、コードの可読性や保守性を損なう可能性があるため、
goto
の使用は一般的に推奨されません。しかし、低レベルのシステムプログラミング、特にランタイムやカーネルのようなパフォーマンスが重視される文脈では、特定の最適化やエラーハンドリングのために使用されることがあります。 - このコミットのコードでは、
select
のロジック内で複数のジャンプ先(asyns
,asynr
,gots
,gotr
,next1
,next2
など)に効率的に分岐するためにgoto
が多用されています。
- C言語における
-
キュー (Queue) とデキュー (Dequeue):
dequeue
関数は、キューから要素を取り出す操作です。チャネルの送受信待ち行列(sendq
,recvq
)からゴルーチンを取り出すために使用されます。
技術的詳細
このコミットの核心は、src/runtime/chan.c
内のsys·selectgo
関数におけるselect
ステートメントの評価ロジックの変更です。特に、default
ケースの処理と、バッファ付き/バッファなしチャネルの送受信操作の優先順位付けが修正されています。
変更前は、select
がチャネル操作を評価する際に、default
ケースが不適切に処理される可能性がありました。これは、チャネルがすぐに準備できる場合でも、default
ケースが誤って選択されたり、逆にdefault
ケースが選択されるべきときにチャネル操作がブロックされたりする原因となったと考えられます。
修正の主なポイントは以下の通りです。
-
default
ケースの早期検出とジャンプ:- 変更前は、
default
ケースが見つかった場合、continue
でループの次のイテレーションに進んでいました。 - 変更後は、
default
ケースが見つかった場合、goto next1;
でループの次のイテレーションに進むように変更されています。これは、default
ケースが他のチャネル操作の評価を妨げないようにするためのものです。
- 変更前は、
-
非同期チャネル (バッファ付きチャネル) の処理ロジックの改善:
- バッファ付きチャネル(
c->dataqsiz > 0
)の場合の送受信ロジックが修正されています。 - 送信操作(
cas->send
が真)の場合、バッファに空きがあればすぐに送信できるため、goto asyns;
で非同期送信処理にジャンプします。変更前は、この後にelse
ブロックで受信処理のチェックが行われていましたが、これは不要または誤りでした。 - 受信操作(
cas->send
が偽)の場合、バッファにデータがあればすぐに受信できるため、goto asynr;
で非同期受信処理にジャンプします。変更前は、この後にelse
ブロックで送信処理のチェックが行われていましたが、これも不要または誤りでした。 - これらの変更により、バッファ付きチャネルの送受信が可能な場合に、
select
がより迅速かつ正確にその操作を選択できるようになりました。
- バッファ付きチャネル(
-
同期チャネル (バッファなしチャネル) の処理ロジックの改善:
- 同期チャネルの場合、対応する送受信ゴルーチンが待ち行列(
recvq
またはsendq
)に存在するかどうかをチェックします。 - 送信操作の場合、
c->recvq
から受信ゴルーチンをデキューし、存在すればgoto gots;
で同期送信処理にジャンプします。 - 受信操作の場合、
c->sendq
から送信ゴルーチンをデキューし、存在すればgoto gotr;
で同期受信処理にジャンプします。 - 変更前は、これらのチェックが
else
ブロック内にネストされており、ロジックが複雑になっていました。変更後は、goto next1;
を導入することで、各チャネル操作のチェックがより直線的になり、不要なパスが排除されています。
- 同期チャネルの場合、対応する送受信ゴルーチンが待ち行列(
-
goto next1;
とgoto next2;
の導入:- これらの
goto
は、select
の各case
を評価した後、次のcase
の評価に進むための共通のジャンプポイントとして機能します。これにより、コードの重複が減り、ロジックが整理されています。特に、default
ケースや非同期チャネル操作がすぐに解決した場合に、残りのcase
を不必要に評価しないようにするための効率的な手段として機能します。
- これらの
全体として、この修正はselect
ステートメントがチャネルの準備状況をより正確に判断し、default
ケースの振る舞いをGo言語のセマンティクスに合致させることを目的としています。特に、default
ケースが存在する場合にselect
がブロックしないという保証を、より堅牢に実現するための変更と言えます。
コアとなるコードの変更箇所
変更はすべてsrc/runtime/chan.c
ファイル内のsys·selectgo
関数に集中しています。
--- a/src/runtime/chan.c
+++ b/src/runtime/chan.c
@@ -581,31 +580,35 @@ sys·selectgo(Select *sel)
dfl = nil;
for(i=0; i<sel->ncase; i++) {
cas = &sel->scase[o];
+
if(cas->send == 2) { // default
dfl = cas;
- continue;
+ goto next1;
}
+
c = cas->chan;
if(c->dataqsiz > 0) {
if(cas->send) {
if(c->qcount < c->dataqsiz)
goto asyns;
- } else {
- if(c->qcount > 0)
- goto asynr;
+ goto next1;
}
- } else
+ if(c->qcount > 0)
+ goto asynr;
+ goto next1;
+ }
if(cas->send) {
sg = dequeue(&c->recvq, c);
if(sg != nil)
goto gots;
- } else {
- sg = dequeue(&c->sendq, c);
- if(sg != nil)
- goto gotr;
+ goto next1;
}
+ sg = dequeue(&c->sendq, c);
+ if(sg != nil)
+ goto gotr;
+ next1:
o += p;
if(o >= sel->ncase)
o -= sel->ncase;
@@ -631,16 +634,17 @@ sys·selectgo(Select *sel)
sg = allocsg(c);
sg->offset = o;
enqueue(&c->sendq, sg);
- } else {
- if(c->qcount > 0) {
- prints("selectgo: pass 2 async recv\\n");
- goto asynr;
- }
- sg = allocsg(c);
- sg->offset = o;
- enqueue(&c->recvq, sg);
+ goto next2;
+ }
+ if(c->qcount > 0) {
+ prints("selectgo: pass 2 async recv\\n");
+ goto asynr;
}
- } else
+ sg = allocsg(c);
+ sg->offset = o;
+ enqueue(&c->recvq, sg);
+ goto next2;
+ }
if(cas->send) {
sg = dequeue(&c->recvq, c);
@@ -653,18 +657,19 @@ sys·selectgo(Select *sel)
sg->offset = o;
c->elemalg->copy(c->elemsize, sg->elem, cas->u.elem);\
enqueue(&c->sendq, sg);
- } else {
- sg = dequeue(&c->sendq, c);
- if(sg != nil) {
- prints("selectgo: pass 2 sync recv\\n");
- g->selgen++;
- goto gotr;
- }
- sg = allocsg(c);
- sg->offset = o;
- enqueue(&c->recvq, sg);
+ goto next2;
}
+ sg = dequeue(&c->sendq, c);
+ if(sg != nil) {
+ prints("selectgo: pass 2 sync recv\\n");
+ g->selgen++;
+ goto gotr;
}
+ sg = allocsg(c);
+ sg->offset = o;
+ enqueue(&c->recvq, sg);
+ next2:
o += p;
if(o >= sel->ncase)
o -= sel->ncase;
コアとなるコードの解説
sys·selectgo
関数は、Goのselect
ステートメントのランタイム実装の中核をなす部分です。この関数は、Select
構造体(sel
)で表現される複数のチャネルケースを評価し、準備ができたケースを実行するか、default
ケースがあればそれを実行します。
変更の主要な部分は、for
ループ内で各case
を反復処理するロジックです。
-
default
ケースの処理:if(cas->send == 2)
は、現在のcase
がdefault
ケースであるかをチェックします(send
フィールドが2はdefault
を意味します)。- 変更前は
continue;
で次のイテレーションに進んでいましたが、これはdefault
ケースが見つかった後も、ループが残りのcase
を評価し続けることを意味します。 - 変更後は
goto next1;
に変更されました。これにより、default
ケースが見つかった場合、すぐにnext1
ラベルにジャンプし、現在のselect
パスの残りの評価をスキップして、次のcase
の評価に進むか、ループを終了してdefault
ケースの処理に移る準備をします。これは、default
ケースが非ブロッキングであることを保証するために重要です。
-
バッファ付きチャネルの送受信処理 (
c->dataqsiz > 0
):- 送信側 (
cas->send
が真):if(c->qcount < c->dataqsiz)
: バッファに空きがあるかチェックします。- 空きがあれば
goto asyns;
で非同期送信処理にジャンプします。 - 重要な変更は、この後に
goto next1;
が追加されたことです。これにより、非同期送信が可能な場合、その後の受信関連のチェックをスキップし、次のcase
の評価に進みます。変更前は、送信可能であっても、その後のelse
ブロックで受信のチェックが行われており、ロジックが不正確でした。
- 受信側 (
cas->send
が偽):if(c->qcount > 0)
: バッファにデータがあるかチェックします。- データがあれば
goto asynr;
で非同期受信処理にジャンプします。 - 同様に、この後に
goto next1;
が追加され、非同期受信が可能な場合に、その後の送信関連のチェックをスキップします。
- 送信側 (
-
同期チャネルの送受信処理:
- 送信側 (
cas->send
が真):sg = dequeue(&c->recvq, c);
: 受信待ちのゴルーチンがあるかチェックします。- あれば
goto gots;
で同期送信処理にジャンプします。 - この後に
goto next1;
が追加され、同期送信が可能な場合に、その後の受信関連のチェックをスキップします。
- 受信側 (
cas->send
が偽):sg = dequeue(&c->sendq, c);
: 送信待ちのゴルーチンがあるかチェックします。- あれば
goto gotr;
で同期受信処理にジャンプします。 - この後に
goto next1;
が追加され、同期受信が可能な場合に、その後の送信関連のチェックをスキップします。
- 送信側 (
これらの変更は、select
が各case
を評価する際のフローを最適化し、特にdefault
ケースの存在下での非ブロッキング動作を保証するために行われました。goto next1;
とgoto next2;
の導入により、コードのパスがより明確になり、不要なチェックが回避されることで、ランタイムの効率と正確性が向上しています。
関連リンク
- Go言語の
select
ステートメントに関する公式ドキュメント(現在のバージョン): - Go言語のチャネルに関する公式ドキュメント(現在のバージョン):
- Go言語の初期のコミット履歴を閲覧できるGitHubリポジトリ:
参考にした情報源リンク
- Go言語の
select
ステートメントの動作に関する一般的な解説記事やチュートリアル。 - Goランタイムの内部構造に関する技術ブログやドキュメント。
- C言語の
goto
ステートメントの使用に関するプログラミングの慣習と理由。 - Go言語の初期の設計に関する議論やメーリングリストのアーカイブ(もし公開されていれば)。
- Go言語のソースコード(特に
src/runtime/chan.c
の現在のバージョンと比較することで、時間の経過による進化を理解できます)。
(注: 2008年当時のGo言語のドキュメントや詳細なバグ報告は、現在のWeb上では見つけにくい場合があります。上記の「関連リンク」は現在の公式ドキュメントへのリンクであり、当時の状況を直接反映しているわけではありませんが、概念的な理解には役立ちます。)