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

[インデックス 1070] ファイルの概要

このコミットは、Go言語のランタイムにおけるチャネルのselectステートメント、特にdefaultケースのサポートを強化するためのものです。src/runtime/chan.cファイルに、selectdefaultケースを処理するための新しいランタイム関数sys·selectdefaultが追加されました。

コミット

commit a6af48432819fcc6c963d1ac883aefe8cd6f7555
Author: Russ Cox <rsc@golang.org>
Date:   Wed Nov 5 18:04:24 2008 -0800

    more runtime support for chan select default

    R=ken
    OCL=18630
    CL=18630

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a6af48432819fcc6c963d1ac883aefe8cd6f7555

元コミット内容

このコミットの元のメッセージは「more runtime support for chan select default」であり、Go言語のチャネルにおけるselectステートメントのdefaultケースに対するランタイムサポートをさらに追加する意図が示されています。

変更の背景

Go言語のselectステートメントは、複数のチャネル操作を待機し、準備ができた最初の操作を実行するために使用されます。selectステートメントにはオプションでdefaultケースを含めることができ、これはどのチャネル操作もすぐに実行できない場合に即座に実行されます。defaultケースがない場合、selectはチャネル操作のいずれかが準備できるまでブロックします。

このコミットが行われた2008年当時、Go言語はまだ開発の初期段階にあり、ランタイムの様々なコンポーネントが構築されていました。selectステートメントのdefaultケースは、ノンブロッキングなチャネル操作を実現するために不可欠な機能です。このコミットは、コンパイラが生成するコードとランタイムが連携してselectdefaultケースを正しく処理できるように、ランタイム側のサポートを強化することを目的としています。

具体的には、selectステートメントがdefaultケースを持つ場合、ランタイムは他のチャネル操作が準備できていないことを検出した際に、このdefaultケースにジャンプする必要があります。このコミットで追加されたsys·selectdefault関数は、このdefaultケースの情報をランタイムのSelect構造体に登録し、selectの実行ロジックがdefaultケースを適切に選択できるようにするためのものです。

また、既存のsys·selectrecv関数内のエラーメッセージの修正も行われています。これは、selectsend(送信)ではなくselectrecv(受信)のコンテキストで「too many cases」というエラーが発生した場合に、より正確なメッセージをスローするようにするためのバグ修正と考えられます。

前提知識の解説

Go言語のチャネル (Channels)

Go言語のチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送信できます。チャネルは、ゴルーチン間の同期と通信を安全に行うための主要な手段であり、共有メモリによる競合状態を避けるために設計されています。

  • チャネルの作成: make(chan int)のように作成します。
  • 送信: ch <- valueのようにチャネルに値を送信します。
  • 受信: value := <-chのようにチャネルから値を受信します。

selectステートメント

selectステートメントは、複数のチャネル操作を同時に待機し、準備ができた最初の操作を実行するために使用されます。これは、Unixのselectシステムコールに似た概念で、複数のI/O操作を監視するのに使われます。

selectの基本的な構文は以下の通りです。

select {
case <-ch1:
    // ch1からの受信が準備できた場合
case ch2 <- value:
    // ch2への送信が準備できた場合
default:
    // どのチャネル操作もすぐに準備できていない場合
}

selectdefaultケース

selectステートメントにdefaultケースが含まれている場合、selectはどのチャネル操作もすぐに実行できない(つまり、チャネルがブロックされる)場合に、defaultケースのコードブロックを即座に実行します。これにより、selectステートメント全体がノンブロッキングになります。defaultケースがない場合、selectはチャネル操作のいずれかが準備できるまでブロックします。

Goランタイムの内部構造(初期のGo)

Go言語のランタイムは、ゴルーチンのスケジューリング、チャネル操作、ガベージコレクションなど、Goプログラムの実行を管理する低レベルのコードです。初期のGoランタイムは主にC言語で書かれており、チャネル操作のようなプリミティブはsrc/runtime/chan.cのようなファイルで実装されていました。

  • Select構造体: selectステートメントの内部状態を管理するためにランタイムが使用する構造体です。これには、各caseの情報(チャネル、操作の種類、関連するデータなど)が格納されます。
  • Scase構造体: Select構造体の一部として、個々のcase(チャネル操作)の詳細を保持します。これには、操作の種類(送信/受信/デフォルト)、関連するチャネル、データポインタなどが含まれます。
  • pc (Program Counter): プログラムカウンタ。特定のコードの実行位置を示します。ランタイムは、selectが完了した後にどのコードに戻るべきかをpcで記録します。
  • so (Stack Offset): スタックオフセット。スタック上の変数の位置を示します。
  • sendフィールド: Scase構造体内のフィールドで、そのcaseが送信操作(1)、受信操作(0)、またはdefault操作(2)であるかを示します。

技術的詳細

このコミットの主要な変更点は、src/runtime/chan.cファイルにsys·selectdefault関数が追加されたことです。この関数は、Goコンパイラによって生成されたコードから呼び出され、selectステートメントのdefaultケースに関する情報をランタイムに登録します。

sys·selectdefault関数の役割

sys·selectdefault関数は、selectステートメントにdefaultケースが存在する場合に、そのdefaultケースの情報をSelect構造体(sel)に設定します。

  1. ケースのインデックス管理: i = sel->ncase; if(i >= sel->tcase) throw("selectdefault: too many cases"); sel->ncase = i+1; これは、現在のselectステートメントで処理されているケースの数を追跡し、新しいdefaultケースをsel->scase配列の次の利用可能なスロットに割り当てます。sel->tcaseは、selectステートメントが持つケースの総数(defaultを含む)を示します。

  2. Scase構造体の初期化: cas = &sel->scase[i]; 新しく割り当てられたScase構造体へのポインタを取得します。

  3. プログラムカウンタ (pc) の設定: cas->pc = sys·getcallerpc(&sel); sys·getcallerpcは、この関数を呼び出した場所のプログラムカウンタ(リターンアドレス)を取得します。これは、selectステートメントがdefaultケースを実行した後に、Goプログラムのどこに戻るべきかをランタイムが知るために使用されます。

  4. チャネル (chan) の設定: cas->chan = c; defaultケースは特定のチャネル操作に関連付けられていないため、cnil(Goのnilに相当)に設定されます。

  5. スタックオフセット (so) の設定: eo = rnd(sizeof(sel), sizeof(c)); eo = rnd(eo+sizeof(c), sizeof(byte*)); cas->so = rnd(eo+sizeof(byte*), 1); これらの行は、スタック上の要素のオフセットを計算しています。rnd関数はアライメントを考慮した丸めを行うユーティリティ関数です。cas->soは、defaultケースに関連するデータ(もしあれば)がスタック上のどこにあるかを示すオフセットです。defaultケースでは通常、チャネル操作のようなデータ転送がないため、このオフセットは主にデバッグや内部的なアライメントのために使用される可能性があります。

  6. 操作の種類 (send) の設定: cas->send = 2; これは非常に重要です。Scase構造体のsendフィールドは、そのケースがどのような操作を表すかを示します。

    • 0: 受信操作
    • 1: 送信操作
    • 2: defaultケース この設定により、ランタイムのselect実行ロジックは、このケースがdefaultケースであることを認識し、他のチャネル操作が準備できていない場合にこれを実行する候補として扱います。
  7. 要素ポインタ (u.elemp) の設定: cas->u.elemp = *(byte**)((byte*)&sel + eo); u.elempは、チャネル操作で送受信される要素へのポインタを格納するために使用される共用体(union)の一部です。defaultケースでは直接的な要素の送受信がないため、この行はスタック上の特定の場所を指すように設定されていますが、defaultケースの実行ロジックでは通常使用されません。これは、Scase構造体の一般的な初期化の一部として行われる可能性があります。

  8. デバッグ出力: if(debug) { ... } デバッグモードが有効な場合、新しく登録されたdefaultケースに関する情報(Selectポインタ、pcchansosend)が標準出力にプリントされます。これは、ランタイムの動作をデバッグするために役立ちます。

エラーメッセージの修正

sys·selectrecv関数内の以下の行が変更されました。

--- a/src/runtime/chan.c
+++ b/src/runtime/chan.c
@@ -469,7 +469,7 @@ sys·selectrecv(Select *sel, Hchan *c, ...)
 
 	i = sel->ncase;
 	if(i >= sel->tcase)
-		throw("selectsend: too many cases");
+		throw("selectrecv: too many cases");
 	sel->ncase = i+1;
 	cas = &sel->scase[i];

これは、sys·selectrecv(チャネルからの受信操作を登録する関数)内で、ケース数が多すぎる場合にスローされるエラーメッセージが誤ってselectsendと表示されていたのを、正しいselectrecvに修正したものです。これは小さなバグ修正ですが、ランタイムのエラーメッセージの正確性を向上させます。

コアとなるコードの変更箇所

src/runtime/chan.cファイルにおいて、以下の変更が行われました。

  1. sys·selectrecv関数内のエラーメッセージの修正(1行変更)。
  2. sys·selectdefault関数の新規追加(39行追加)。
// 修正箇所
@@ -469,7 +469,7 @@ sys·selectrecv(Select *sel, Hchan *c, ...)
 
 	i = sel->ncase;
 	if(i >= sel->tcase)
-		throw("selectsend: too many cases");
+		throw("selectrecv: too many cases");
 	sel->ncase = i+1;
 	cas = &sel->scase[i];
 
// 追加箇所
+void
+sys·selectdefault(Select *sel)
+{
+	int32 i, eo;
+	Scase *cas;
+	Hchan *c;
+	
+	c = nil;
+	i = sel->ncase;
+	if(i >= sel->tcase)
+		throw("selectdefault: too many cases");
+	sel->ncase = i+1;
+	cas = &sel->scase[i];
+
+	cas->pc = sys·getcallerpc(&sel);
+	cas->chan = c;
+
+	eo = rnd(sizeof(sel), sizeof(c));
+	eo = rnd(eo+sizeof(c), sizeof(byte*));
+	cas->so = rnd(eo+sizeof(byte*), 1);
+	cas->send = 2;
+	cas->u.elemp = *(byte**)((byte*)&sel + eo);
+
+	if(debug) {
+		prints("newselect s=");
+		sys·printpointer(sel);
+		prints(" pc=");
+		sys·printpointer(cas->pc);
+		prints(" chan=");
+		sys·printpointer(cas->chan);
+		prints(" so=");
+		sys·printint(cas->so);
+		prints(" send=");
+		sys·printint(cas->send);
+		prints("\n");
+	}
+}

コアとなるコードの解説

sys·selectdefault関数

この関数は、Goコンパイラがselectステートメントのdefaultケースを検出した際に、ランタイムにその情報を登録するために呼び出されます。

  • Select *sel: 現在のselectステートメントの全体の状態を保持するSelect構造体へのポインタです。
  • int32 i, eo;: iScase配列のインデックス、eoはスタックオフセット計算用の一時変数です。
  • Scase *cas;: 現在処理しているdefaultケースに対応するScase構造体へのポインタです。
  • Hchan *c;: チャネルへのポインタですが、defaultケースでは特定のチャネルに関連しないためnilに設定されます。

関数の主要なロジックは以下の通りです。

  1. sel->ncaseをインクリメントし、sel->scase配列の次の空きスロットにdefaultケースを登録します。sel->tcaseを超過するとエラーをスローします。
  2. cas->pc = sys·getcallerpc(&sel);: defaultケースが実行された後に制御が戻るべきプログラムカウンタ(呼び出し元のGoコードの場所)を記録します。
  3. cas->chan = c;: defaultケースはチャネル操作ではないため、チャネルポインタはnilに設定されます。
  4. cas->so = ...;: スタックオフセットを計算し設定します。これは、defaultケースに関連するデータ(もしあれば)がスタック上のどこにあるかを示すためのものです。
  5. cas->send = 2;: この行が最も重要です。sendフィールドに2を設定することで、このScasedefaultケースであることをランタイムに明示的に伝えます。ランタイムのselectロジックは、この値を見て、他のチャネル操作が準備できていない場合にdefaultケースを実行する判断を下します。
  6. cas->u.elemp = ...;: u.elempはチャネル操作で送受信される要素へのポインタを格納するために使用されますが、defaultケースでは直接的な要素の送受信がないため、一般的な初期化の一部として設定されます。
  7. if(debug)ブロック: デバッグビルドの場合、登録されたdefaultケースの詳細情報がプリントされ、ランタイムの動作を追跡するのに役立ちます。

このsys·selectdefault関数が追加されたことで、Goコンパイラはselectステートメントにdefaultケースがある場合にこの関数を呼び出すようになり、ランタイムはdefaultケースの存在と位置を正確に把握できるようになりました。これにより、selectステートメントのノンブロッキングな動作がランタイムレベルで適切にサポートされるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(特にsrc/runtime/chan.cの歴史的なバージョン)
  • Go言語のselectステートメントとチャネルに関する一般的な知識
  • Go言語の初期のランタイム実装に関する議論やドキュメント(もし公開されていれば)
  • Go言語のselectステートメントの内部動作に関する技術記事やブログポスト(一般的な概念理解のため)
  • Go言語のselectステートメントのdefaultケースの動作に関する情報