[インデックス 1069] ファイルの概要
このコミットは、Go言語のランタイムにおける select
ステートメントの default
ケースのサポートを追加するものです。具体的には、チャネル操作が準備できていない場合に即座に実行される default
ブロックの挙動を、ランタイムレベルで実現するための変更が含まれています。
コミット
commit b69e80d8dd683600c70a334da52fc0cd8a56e739
Author: Russ Cox <rsc@golang.org>
Date: Wed Nov 5 17:57:18 2008 -0800
runtime support for default in select.
assumes cas->send == 2 for default case.
R=ken
OCL=18628
CL=18628
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b69e80d8dd683600c70a334d52fc0cd8a56e739
元コミット内容
このコミットは、Go言語の select
ステートメントにおける default
ケースのランタイムサポートを導入します。default
ケースは、Scase
構造体の send
フィールドが 2
であると仮定して処理されます。
変更の背景
Go言語の select
ステートメントは、複数のチャネル操作を待機し、準備ができた最初の操作を実行するための強力な並行処理プリミティブです。しかし、初期のGo言語では、select
が常にブロックする挙動しか持たず、チャネル操作が準備できていない場合に即座に処理を続行する非ブロッキングな select
を実現する方法がありませんでした。
このコミットが行われた2008年11月は、Go言語がまだ一般に公開される前の開発初期段階にあたります。この時期に、select
ステートメントに default
ケースの概念を導入することで、開発者はチャネル操作が利用可能でない場合に、別の処理を実行したり、タイムアウトを実装したりするなど、より柔軟な並行処理パターンを記述できるようになりました。これは、Goの並行処理モデルをより実用的で表現豊かなものにするための重要なステップでした。
前提知識の解説
Go言語の並行処理とチャネル
Go言語は、CSP (Communicating Sequential Processes) に基づく並行処理モデルを採用しています。これは、共有メモリによる同期ではなく、チャネルを通じたゴルーチン間の通信によって並行処理を実現するという考え方です。
- ゴルーチン (Goroutine): Goランタイムによって管理される軽量なスレッドのようなものです。数千、数万のゴルーチンを同時に実行してもオーバーヘッドが少ないのが特徴です。
- チャネル (Channel): ゴルーチン間で値を送受信するための通信路です。チャネルは型付けされており、
make(chan Type)
で作成します。チャネルへの送信 (ch <- value
) や受信 (value <- ch
) は、デフォルトでブロッキング操作です。
select
ステートメント
select
ステートメントは、複数のチャネル操作を同時に待機し、そのうちのいずれかが準備できた場合に、その操作に対応するケースを実行します。
select {
case <-ch1:
// ch1 から値を受信
case ch2 <- value:
// ch2 へ値を送信
case <-time.After(5 * time.Second):
// 5秒後にタイムアウト
default:
// どのチャネル操作も準備できていない場合に実行
}
select
は以下のルールで動作します。
- 複数の
case
が同時に準備できた場合、ランダムに1つが選択されます。 - どの
case
も準備できていない場合、default
ケースが存在すればそれが実行されます。 default
ケースが存在せず、どのcase
も準備できていない場合、select
ステートメントはチャネル操作のいずれかが準備できるまでブロックします。
このコミットは、上記の「2. default
ケースが存在すればそれが実行されます」という挙動をランタイムレベルでサポートするためのものです。
Goランタイムの内部構造 (初期のGo)
Go言語のランタイムは、Goプログラムの実行を管理するC言語(およびアセンブリ言語)で書かれた部分です。チャネル、スケジューラ、ガベージコレクタなどの低レベルな機能は、このランタイムによって提供されます。
src/runtime/chan.c
: このファイルは、Goのチャネルの基本的な操作(作成、送信、受信、クローズなど)をC言語で実装しています。select
ステートメントの内部処理もこのファイルに含まれています。Scase
構造体:select
ステートメント内の個々のcase
を表現するためにランタイムが使用する内部構造体です。各case
がどのチャネルを参照しているか、送信操作か受信操作か、などの情報を含みます。
技術的詳細
このコミットの主要な変更は、src/runtime/chan.c
ファイル内の Scase
構造体と sys·selectgo
関数にあります。
Scase
構造体の拡張
Scase
構造体は、select
ステートメント内の各 case
を表現するためにランタイムが使用する内部データ構造です。この構造体には、チャネルへのポインタ、リターンPC(プログラムカウンタ)、そして操作の種類を示す send
フィールドが含まれています。
変更前は、send
フィールドは以下の意味を持っていました。
0
: 受信操作 (receive)1
: 送信操作 (send)
このコミットでは、default
ケースを識別するために、send
フィールドに新しい値 2
が導入されました。これにより、ランタイムは Scase
インスタンスが通常のチャネル操作ではなく、default
ケースを表していることを認識できるようになります。
sys·selectgo
関数の変更
sys·selectgo
関数は、Goの select
ステートメントがコンパイル時に呼び出すランタイム関数です。この関数は、select
ステートメント内のすべての case
を評価し、適切なチャネル操作を実行するか、default
ケースがあればそれを実行する役割を担います。
変更の核心は、sys·selectgo
が default
ケースを特別に処理するように修正された点です。
-
default
ケースの識別と分離: 関数はまず、select
ステートメント内のすべてのcase
をループで走査します。この際、cas->send == 2
であるScase
インスタンス(つまりdefault
ケース)が見つかった場合、そのScase
へのポインタをdfl
という新しい変数に保存し、現在のループイテレーションをスキップします。これにより、最初のパスでは通常のチャネル操作のみが評価され、default
ケースは一時的に除外されます。 -
非ブロッキング評価と
default
へのフォールバック: 最初のパスでは、各チャネルが既に準備ができているか(例:バッファ付きチャネルにデータがある、または受信者が待機している)がチェックされます。もし準備ができているチャネル操作が見つかれば、その操作が選択され、select
は終了します。 しかし、最初のパスでどのチャネル操作も準備ができていなかった場合、select
は通常、チャネル操作が準備できるまでブロックします。このコミットでは、このブロッキングの前にdfl != nil
(つまりdefault
ケースが存在する) かどうかをチェックするロジックが追加されました。 -
default
ケースの実行: もしdefault
ケースが存在し、かつ他のどのチャネル操作も即座に準備できていなかった場合、cas
ポインタはdfl
に設定され、goto retc;
ステートメントによって、select
の結果を返す共通の処理パスにジャンプします。これにより、default
ケースが非ブロッキングで実行されることが保証されます。
このメカニズムにより、select
はまず非ブロッキングでチャネルを試行し、どれも成功しない場合にのみ default
ケースにフォールバックするという、Go言語の select
の期待される挙動が実現されました。
コアとなるコードの変更箇所
--- a/src/runtime/chan.c
+++ b/src/runtime/chan.c
@@ -52,7 +52,7 @@ struct Scase
{
Hchan* chan; // chan
byte*\tpc; // return pc
- uint16 send; // 0-recv 1-send
+ uint16 send; // 0-recv 1-send 2-default
uint16 so; // vararg of selected bool
union {
byte elem[8]; // element (send)
@@ -504,7 +504,7 @@ void
sys·selectgo(Select *sel)
{
uint32 p, o, i;
- Scase *cas;
+ Scase *cas, *dfl;
Hchan *c;
SudoG *sg;
G *gp;
@@ -542,8 +542,13 @@ sys·selectgo(Select *sel)
lock(&chanlock);
// pass 1 - look for something already waiting
+ dfl = nil;
for(i=0; i<sel->ncase; i++) {
cas = &sel->scase[o];
+ if(cas->send == 2) { // default
+ dfl = cas;
+ continue;
+ }
c = cas->chan;
if(c->dataqsiz > 0) {
if(cas->send) {
@@ -569,6 +574,12 @@ sys·selectgo(Select *sel)
if(o >= sel->ncase)
o -= sel->ncase;
}
+
+ if(dfl != nil) {
+ cas = dfl;
+ goto retc;
+ }
+
// pass 2 - enqueue on all chans
for(i=0; i<sel->ncase; i++) {
コアとなるコードの解説
struct Scase
の変更
- uint16 send; // 0-recv 1-send
+ uint16 send; // 0-recv 1-send 2-default
Scase
構造体の send
フィールドのコメントが更新され、2-default
という新しい値が追加されました。これは、send
フィールドが 2
の場合、その Scase
インスタンスが select
ステートメントの default
ケースを表すことを明示しています。これにより、ランタイムは default
ケースを他のチャネル操作と区別できるようになります。
sys·selectgo
関数の変更
uint32 p, o, i;
- Scase *cas;
+ Scase *cas, *dfl; // dfl (default) ポインタが追加
Hchan *c;
SudoG *sg;
G *gp;
sys·selectgo
関数内に、Scase *dfl;
という新しいポインタ変数が宣言されました。この dfl
ポインタは、select
ステートメント内に default
ケースが存在する場合に、その Scase
インスタンスを指すために使用されます。
lock(&chanlock);
// pass 1 - look for something already waiting
+ dfl = nil; // dfl を nil で初期化
for(i=0; i<sel->ncase; i++) {
cas = &sel->scase[o];
+ if(cas->send == 2) { // default
+ dfl = cas; // default ケースが見つかったら dfl に保存
+ continue; // このパスでは default ケースをスキップ
+ }
c = cas->chan;
if(c->dataqsiz > 0) {
if(cas->send) {
sys·selectgo
の最初のループ (pass 1
) は、既に準備ができているチャネル操作を探します。このループの冒頭で dfl = nil;
と初期化されます。
ループ内で、現在の cas
(select case) の send
フィールドが 2
であるかどうかがチェックされます。もし 2
であれば、それは default
ケースであるため、その cas
を dfl
ポインタに保存し、continue
で現在のイテレーションをスキップします。これにより、default
ケースは最初の「準備ができているチャネルを探す」パスでは評価されず、後で特別な処理が施されることになります。
// ... (pass 1 の残りのロジック) ...
// pass 1 で何も見つからなかった場合、このブロックが実行される
+ if(dfl != nil) { // default ケースが存在するかチェック
+ cas = dfl; // 存在すれば cas を default ケースに設定
+ goto retc; // retc (リターン処理) へジャンプ
+ }
+
// pass 2 - enqueue on all chans (チャネルが準備できるまでブロックする処理)
for(i=0; i<sel->ncase; i++) {
最初のループ (pass 1
) が終了した後、もしどのチャネル操作も即座に準備できていなかった場合、この新しいブロックが実行されます。
if(dfl != nil)
は、select
ステートメントに default
ケースが含まれていたかどうかをチェックします。
もし dfl
が nil
でなければ(つまり default
ケースが存在すれば)、cas
ポインタを dfl
が指す default
ケースに設定し、goto retc;
を使用して関数の終了処理(select
の結果を返す部分)に直接ジャンプします。
この goto
により、select
はチャネルが準備できるまでブロックすることなく、直ちに default
ケースの処理を実行し、sys·selectgo
関数から戻ることができます。これは、select
の default
ケースが非ブロッキングであるという重要な特性を実現しています。
関連リンク
- Go言語の
select
ステートメントに関する公式ドキュメント (現代のGo): https://go.dev/tour/concurrency/5 - Go言語のチャネルに関する公式ドキュメント (現代のGo): https://go.dev/tour/concurrency/2
- Go言語のソースコードリポジトリ: https://github.com/golang/go
参考にした情報源リンク
- Go言語の初期の設計に関する議論やメーリングリストのアーカイブ(Go言語がオープンソース化された後の情報が主だが、初期の設計思想を理解する上で参考になる場合がある)
- Go言語のランタイムに関する技術ブログや解説記事(
chan.c
のような低レベルな実装を解説しているもの) - CSP (Communicating Sequential Processes) に関する文献(Goの並行処理モデルの基礎)