[インデックス 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上では見つけにくい場合があります。上記の「関連リンク」は現在の公式ドキュメントへのリンクであり、当時の状況を直接反映しているわけではありませんが、概念的な理解には役立ちます。)