[インデックス 18468] ファイルの概要
このコミットは、Goランタイムにおけるチャネル(chan
)関連のコードをリファクタリングし、その内部実装を改善するものです。主に、内部関数の静的化、G
(ゴルーチン)構造体からのselgen
フィールドの削除とselect
処理の効率化、そしてchansend
/chanrecv
関数のパラメータの簡素化が行われています。
コミット
commit e1ee04828d94e8673f13cd854245920cdea27acc
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Feb 12 22:21:38 2014 +0400
runtime: refactor chan code
1. Make internal chan functions static.
2. Move selgen local variable instead of a member of G struct.
3. Change "bool *pres/selected" parameter of chansend/chanrecv to "bool block",
which is simpler, faster and less code.
-37 lines total.
LGTM=rsc
R=golang-codereviews, dave, gobot, rsc
CC=bradfitz, golang-codereviews, iant, khr
https://golang.org/cl/58610043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e1ee04828d94e8673f13cd854245920cdea27acc
元コミット内容
このコミットは、Goランタイムのチャネルコードを以下の3つの主要な点においてリファクタリングします。
- 内部チャネル関数の静的化:
runtime·makechan_c
,runtime·chansend
,runtime·chanrecv
といったチャネル操作のコア関数が、static
キーワードを用いて内部関数として定義されるようになりました。これにより、これらの関数はchan.c
ファイル内でのみアクセス可能となり、カプセル化が強化され、グローバルな名前空間の汚染が防がれます。 G
構造体からのselgen
の移動:G
(ゴルーチン)構造体からselgen
(select generation)フィールドが削除されました。代わりに、select
操作に関連するSudoG
構造体内でselectdone
というポインタが導入され、select
の完了状態をより効率的に管理するようになりました。これは、select
文におけるゴルーチンの状態管理を簡素化し、パフォーマンスを向上させることを目的としています。chansend
/chanrecv
のパラメータ変更:chansend
およびchanrecv
関数のbool *pres
(present)やbool *selected
といったポインタ引数が、bool block
という単一のブーリアン引数に置き換えられました。これにより、関数のインターフェースが簡素化され、コードの可読性と効率が向上します。block
引数は、チャネル操作が即座に完了できない場合にブロックするかどうかを直接的に示します。
変更の背景
このコミットの背景には、Goランタイムのチャネル実装における効率性、保守性、およびコードの簡潔性の向上が挙げられます。
- カプセル化の強化: 以前はグローバルに公開されていたチャネル関連の内部関数を
static
にすることで、ランタイムの他の部分からの不必要な依存を防ぎ、コードのモジュール性を高める狙いがあります。これにより、将来的な変更が他の部分に与える影響を局所化できます。 select
処理の最適化:select
文はGoの並行処理において非常に強力な機能ですが、その内部実装は複雑になりがちです。特に、複数のチャネル操作を待機する際に、どの操作が選択されたか、あるいはゴルーチンがすでに他のselect
ケースによって処理されたかどうかを効率的に追跡する必要がありました。G
構造体からselgen
を削除し、SudoG
構造体内のselectdone
ポインタとruntime·cas
(Compare-And-Swap)操作を用いることで、この調整メカニズムがより直接的かつアトミックになり、競合状態の管理が簡素化され、パフォーマンスが向上します。- APIの簡素化と効率化:
chansend
とchanrecv
のパラメータをbool *pres/selected
からbool block
に変更することは、関数の呼び出し規約を簡素化し、ポインタのデリファレンスといったオーバーヘッドを削減します。これにより、これらの頻繁に呼び出される関数の実行がわずかに高速化され、コードの記述もより直感的になります。block
という明確な名前は、関数の振る舞いを一目で理解しやすくします。
これらの変更は、Goの並行処理プリミティブであるチャネルの堅牢性と効率性をさらに高めるための、継続的なランタイム最適化の一環として行われました。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムおよび並行処理に関する前提知識が必要です。
-
Goのチャネル (Channels):
- Goにおけるチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。
make(chan Type)
で作成され、ch <- value
で送信、value <- ch
で受信します。 - チャネルはバッファリングされている場合(
make(chan Type, capacity)
)とされていない場合(バッファなしチャネル)があります。バッファなしチャネルでは、送信と受信が同時に行われる(同期する)まで、ゴルーチンはブロックされます。 - チャネルは、Goのメモリモデルにおいて、ゴルーチン間の同期と通信を安全に行うための主要な手段です。
- Goにおけるチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。
-
ゴルーチン (Goroutines):
- Goの軽量な実行スレッドです。
go
キーワードを使って関数を呼び出すことで起動されます。 - ランタイムスケジューラによって管理され、OSのスレッドに多重化されます。
- Goの軽量な実行スレッドです。
-
select
文:- 複数のチャネル操作(送信または受信)を同時に待機し、準備ができた最初の操作を実行するためのGoの制御構造です。
select
文内の各case
はチャネル操作に対応し、default
ケースはどのチャネル操作も準備ができていない場合に即座に実行されます。select
は、非同期I/O、タイムアウト、複数のイベントソースからの処理など、複雑な並行処理パターンを実装するために不可欠です。
-
Hchan
構造体:- Goランタイム内部でチャネルを表すC言語の構造体です。チャネルのタイプ、バッファ、送受信キュー(
sendq
,recvq
)、ロックなどの情報を含みます。
- Goランタイム内部でチャネルを表すC言語の構造体です。チャネルのタイプ、バッファ、送受信キュー(
-
G
構造体:- Goランタイム内部でゴルーチンを表すC言語の構造体です。スタック情報、スケジューリング状態、関連する
M
(OSスレッド)へのポインタなど、ゴルーチンの実行コンテキストを保持します。
- Goランタイム内部でゴルーチンを表すC言語の構造体です。スタック情報、スケジューリング状態、関連する
-
SudoG
構造体:select
操作に参加するゴルーチン(G
)を待機キュー(sendq
やrecvq
)に登録する際に使用される補助的な構造体です。SudoG
は、待機しているゴルーチンへのポインタ、送信/受信する要素、そしてselect
操作のコンテキスト情報(このコミット以前はselgen
、以降はselectdone
)を含みます。
-
runtime·cas
(Compare-And-Swap):- アトミック操作の一種で、特定のメモリ位置の現在の値が期待する値と一致する場合にのみ、そのメモリ位置を新しい値に更新します。これは、複数のゴルーチンが共有データに同時にアクセスする際の競合状態を防ぐために、ロックフリープログラミングでよく使用されます。
-
static
キーワード (C言語):- C言語において、関数に
static
を付けると、その関数は定義されているファイル内でのみ可視となり、他のファイルからは呼び出せなくなります。これにより、名前の衝突を防ぎ、カプセル化を促進します。
- C言語において、関数に
技術的詳細
このコミットは、Goランタイムのチャネル実装におけるいくつかの重要な技術的側面を改善しています。
-
内部関数の静的化:
runtime·makechan_c
,runtime·chansend
,runtime·chanrecv
といった関数は、Goのユーザーコードから直接呼び出されるものではなく、コンパイラによって生成されたコードやランタイムの他の部分から内部的に呼び出されるものです。- これらの関数に
static
キーワードを付与することで、シンボルテーブルからこれらの関数がエクスポートされなくなり、リンカがこれらの関数を他のオブジェクトファイルから参照できなくなります。これにより、ランタイムの内部構造がより明確になり、意図しない外部からのアクセスを防ぎます。また、コンパイラがこれらの関数呼び出しを最適化しやすくなる可能性があります(例えば、インライン化の機会が増えるなど)。
-
selgen
からselectdone
への移行とselect
の同期メカニズム:- 変更前 (
selgen
): 以前のselect
実装では、G
構造体(ゴルーチン)にselgen
というuint32
型のフィールドがありました。これは「select generation number」の略で、ゴルーチンがselect
文に参加するたびにインクリメントされるカウンターのようなものでした。SudoG
構造体もselgen
フィールドを持ち、select
操作が待機キューにゴルーチンを登録する際に、そのゴルーチンの現在のselgen
値をコピーしていました。select
がチャネル操作を選択し、対応するゴルーチンをready
状態にする際、そのゴルーチンのselgen
がSudoG
に保存された値と一致するかどうかを確認していました。もし一致しない場合(例えば、そのゴルーチンが別のselect
操作によってすでに選択され、selgen
が更新されていた場合)、そのSudoG
は「stale」(古い、無効な)と判断され、無視されました。これは、複数のselect
が同じチャネルを待機している場合に、二重にゴルーチンをready
にしたり、無効なゴルーチンを処理したりするのを防ぐためのメカニズムでした。
- 変更後 (
selectdone
): このコミットでは、G.selgen
が削除され、SudoG
構造体のselgen
フィールドがuint32* selectdone
ポインタに置き換えられました。selectgo
関数(select
文のランタイム実装)内で、done
というローカル変数(uint32 done = 0;
)が導入されます。select
がチャネルの待機キューにSudoG
を登録する際、sg->selectdone = &done;
として、このローカル変数done
のアドレスをSudoG
に保存します。- チャネル操作が完了し、待機しているゴルーチン(
SudoG
)をready
にする際、sg->selectdone != nil
であることを確認し、さらに*sg->selectdone != 0 || !runtime·cas(sg->selectdone, 0, 1)
という条件でアトミックにdone
変数を更新しようとします。runtime·cas(sg->selectdone, 0, 1)
は、*sg->selectdone
の値が0
であれば1
に更新し、成功した場合はtrue
を返します。- このメカニズムにより、
select
文内の複数のcase
が同時に準備できた場合でも、最初にdone
変数を0
から1
にアトミックに設定できたcase
のみが実際にゴルーチンをready
にし、他のcase
は「stale」として無視されます。
- このアプローチは、
selgen
カウンターのインクリメントとチェックよりも直接的で、select
操作の完了状態をより効率的に、かつ競合なく管理できます。G
構造体からフィールドを削除することで、G
構造体のサイズをわずかに削減し、キャッシュ効率にも寄与する可能性があります。
- 変更前 (
-
chansend
/chanrecv
のパラメータ簡素化:- 変更前:
chansend
はbool *pres
(送信が成功したかどうかを示すポインタ)、chanrecv
はbool *selected
(受信が成功したかどうかを示すポインタ)とbool *received
(値が受信されたかどうかを示すポインタ)を受け取っていました。これらのポインタは、呼び出し元が提供したブーリアン変数のアドレスを指し、関数内でその変数の値を更新していました。 - 変更後:
chansend
とchanrecv
は、bool block
という単一のブーリアン引数を受け取るようになりました。block
がtrue
の場合、操作はブロックする可能性があります(バッファが満杯/空、または相手が準備できていない場合)。block
がfalse
の場合、操作はブロックせず、即座に結果を返します(非ブロック操作)。- 関数の戻り値が
bool
型になり、操作が成功したかどうかを直接返します。例えば、chansend
は送信が成功したらtrue
、失敗したらfalse
を返します。chanrecv
も同様です。
- この変更により、呼び出し元はポインタを渡す必要がなくなり、コードが簡潔になります。また、ポインタのデリファレンスが不要になるため、わずかながら実行効率も向上します。
selectnbsend
やselectnbrecv
のような非ブロック操作を行う関数は、block
引数にfalse
を渡すように変更されました。
- 変更前:
これらの変更は、Goランタイムのチャネル実装の内部的な複雑さを軽減し、より堅牢で効率的な並行処理プリミティブを提供することに貢献しています。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/runtime/chan.c
とsrc/pkg/runtime/runtime.h
の2つのファイルに集中しています。
src/pkg/runtime/chan.c
- 関数の静的化:
runtime·makechan_c
がstatic makechan
に変更。runtime·chansend
がstatic bool chansend
に変更。runtime·chanrecv
がstatic bool chanrecv
に変更。
SudoG
構造体の変更:SudoG
構造体からuint32 selgen;
が削除され、代わりにuint32* selectdone;
が追加。
chansend
関数の変更:- シグネチャが
void runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc)
からstatic bool chansend(ChanType *t, Hchan *c, byte *ep, bool block, void *pc)
に変更。 *pres
への書き込みロジックが削除され、代わりにreturn true;
またはreturn false;
で結果を返すように変更。if(pres != nil)
のチェックがif(!block)
に変更。mysg.selgen = NOSELGEN;
がmysg.selectdone = nil;
に変更。
- シグネチャが
chanrecv
関数の変更:- シグネチャが
void runtime·chanrecv(ChanType *t, Hchan* c, byte *ep, bool *selected, bool *received)
からstatic bool chanrecv(ChanType *t, Hchan* c, byte *ep, bool block, bool *received)
に変更。 *selected
への書き込みロジックが削除され、代わりにreturn true;
またはreturn false;
で結果を返すように変更。if(selected != nil)
のチェックがif(!block)
に変更。mysg.selgen = NOSELGEN;
がmysg.selectdone = nil;
に変更。
- シグネチャが
selectgo
関数の変更:uint32 done;
というローカル変数が追加。sg->selgen = g->selgen;
がsg->selectdone = &done;
に変更。SudoG
の待機キューからの取り出しロジックで、sgp->selgen
のチェックがif(sgp->selectdone != nil)
とif(*sgp->selectdone != 0 || !runtime·cas(sgp->selectdone, 0, 1))
に変更。
runtime·chansend1
,runtime·chanrecv1
,runtime·chanrecv2
,runtime·selectnbsend
,runtime·selectnbrecv
,runtime·selectnbrecv2
,reflect·chansend
,reflect·chanrecv
の呼び出し箇所の変更:- これらの関数内で、静的化された
chansend
やchanrecv
を直接呼び出すように変更。 bool *pres/selected
の代わりにbool block
引数を適切に渡すように変更。特に非ブロック操作ではfalse
を渡す。
- これらの関数内で、静的化された
src/pkg/runtime/runtime.h
G
構造体の変更:struct G
からuint32 selgen;
フィールドが削除。
- チャネル関連関数のプロトタイプ宣言の削除:
Hchan* runtime·makechan_c(ChanType*, int64);
void runtime·chansend(ChanType*, Hchan*, byte*, bool*, void*);
void runtime·chanrecv(ChanType*, Hchan*, byte*, bool*, bool*);
これらの宣言が削除されました。これは、対応する関数がstatic
になり、外部から直接呼び出されなくなったためです。
コアとなるコードの解説
このコミットのコアとなる変更は、Goのチャネル操作の内部ロジックと、特にselect
文の効率的な実装にあります。
chansend
と chanrecv
の変更
変更後のchansend
とchanrecv
関数は、static
キーワードによってファイルスコープに限定され、外部からの直接アクセスを防ぎます。最も重要な変更は、bool *pres
やbool *selected
といったポインタ引数がbool block
というブーリアン引数に置き換えられた点です。
変更前:
void runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc)
// ...
if(pres != nil) {
*pres = true; // または false
return;
}
この形式では、呼び出し元はbool
変数のアドレスを渡し、関数はそのアドレスが指す値を更新します。これは、関数が複数の値を「返す」必要がある場合にC言語でよく使われるパターンです。
変更後:
static bool chansend(ChanType *t, Hchan *c, byte *ep, bool block, void *pc)
// ...
if(!block) { // 以前の if(pres != nil) に相当
runtime·unlock(c);
return false; // 非ブロック操作で即座に失敗
}
// ...
return true; // 操作成功
新しい形式では、block
引数が操作がブロック可能かどうかを直接制御します。block
がfalse
の場合(非ブロック操作)、関数は即座にfalse
を返して失敗を示します。操作が成功した場合はtrue
を返します。これにより、関数のシグネチャが簡素化され、呼び出し元はポインタのデリファレンスを気にする必要がなくなります。これは、Goのユーザーコードがチャネル操作を行う際のok
ブーリアン(例: v, ok := <-ch
)のランタイムレベルでの対応と考えることができます。
select
処理の変更 (selgen
からselectdone
へ)
select
文のランタイム実装であるselectgo
関数におけるSudoG
の処理が大きく変わりました。
変更前 (selgen
):
struct SudoG {
G* g;
uint32 selgen; // gとselgenでgへの弱いポインタを構成
// ...
};
// selectgo内で
sg->selgen = g->selgen; // 現在のゴルーチンのselgenをSudoGにコピー
// 待機キューからSudoGを取り出す際
if(sgp->selgen != NOSELGEN &&
(sgp->selgen != sgp->g->selgen ||
!runtime·cas(&sgp->g->selgen, sgp->selgen, sgp->selgen + 2))) {
// sgpが古い場合、無視する
goto loop;
}
このロジックは、SudoG
が指すゴルーチンが、そのselect
操作が開始されてから別のselect
操作によってすでに処理されていないかを確認するために、ゴルーチンのselgen
カウンターとSudoG
に保存されたselgen
を比較していました。runtime·cas
は、selgen
をアトミックに更新し、複数のselect
ケースが同時に準備できた場合に、最初にselgen
を更新できたケースのみが処理を進めることを保証していました。
変更後 (selectdone
):
struct SudoG {
G* g;
uint32* selectdone; // selectの完了状態を示すポインタ
// ...
};
// selectgo内で
uint32 done = 0; // ローカル変数
// ...
sg->selectdone = &done; // SudoGにローカル変数doneのアドレスを保存
// 待機キューからSudoGを取り出す際
if(sgp->selectdone != nil) {
// sgpがselectに参加しており、すでにシグナルされている場合、無視する
if(*sgp->selectdone != 0 || !runtime·cas(sgp->selectdone, 0, 1))
goto loop;
}
この新しいメカニズムでは、selectgo
関数内でdone
というローカル変数が導入され、各SudoG
はそのdone
変数へのポインタを保持します。select
操作がチャネルから値を受信したり、チャネルに値を送信したりして成功した場合、そのSudoG
に対応するselectdone
ポインタが指すdone
変数をruntime·cas
を使って0
から1
にアトミックに設定しようとします。
*sgp->selectdone != 0
: これは、すでに他のselect
ケースがこのdone
変数を1
に設定している(つまり、このselect
操作がすでに完了している)ことを意味します。!runtime·cas(sgp->selectdone, 0, 1)
: これは、done
変数を0
から1
にアトミックに設定しようとしたが失敗したことを意味します。これも、他のselect
ケースが先に設定したことを示唆します。
どちらの条件も真の場合、そのSudoG
は「stale」と判断され、無視されます。このアプローチは、selgen
カウンターのグローバルなG
構造体からの依存をなくし、select
操作の同期をより局所的かつ効率的に行えるようにします。done
変数はselectgo
のスタック上に存在するため、select
が終了すると自動的にクリーンアップされます。
これらの変更は、Goの並行処理の基盤であるチャネルとselect
の内部実装をより洗練させ、パフォーマンスと保守性を向上させるための重要なステップです。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
- Goのチャネルに関する公式ブログ記事 (A Tour of Go - Concurrency): https://go.dev/tour/concurrency/2
- Goの
select
に関する公式ブログ記事 (A Tour of Go - Select): https://go.dev/tour/concurrency/5 - Goランタイムのソースコード (GitHub): https://github.com/golang/go
参考にした情報源リンク
- Goのコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/58610043
はGerritの変更リストへのリンクです) - Goのチャネル実装に関する技術記事やブログ(一般的なGoのチャネルの仕組みを理解するために参照)
- "Go's work-stealing scheduler" by Dmitry Vyukov: https://go.dev/blog/go1.2scheduler (直接このコミットに関するものではないが、ランタイムの背景理解に役立つ)
- "The Go scheduler" by Kavya Joshi: https://go.dev/blog/go1.14-scheduler (同上)
- C言語の
static
キーワードに関する情報 - アトミック操作とCAS (Compare-And-Swap) に関する情報
- "Atomic operations in Go": https://go.dev/src/sync/atomic/doc.go (Goの
sync/atomic
パッケージのドキュメント) - 一般的なCASの概念に関するコンピュータサイエンスの資料
- "Atomic operations in Go": https://go.dev/src/sync/atomic/doc.go (Goの
- Goの
select
文の内部動作に関する詳細な解説記事(例: "Go's select statement" by Dave Cheney, "Understanding Go's select statement" by Eli Benderskyなど、具体的なURLは変動するため一般的な検索で参照)- これらの記事は、
selgen
やSudoG
といった内部構造の役割を理解する上で非常に有用です。 - 特に、
select
がどのように複数のチャネル操作を効率的に処理し、競合状態を回避しているかについての洞察を提供します。 - このコミット以前の
select
実装と、このコミットによる改善点を比較する上で、過去の資料が役立ちます。
- これらの記事は、
- Goの
G
構造体とゴルーチン管理に関する情報(ランタイムの内部構造を理解するために参照)- Goのソースコード内の
src/runtime/runtime2.go
やsrc/runtime/proc.go
などのファイルが関連します。 G
構造体はGoランタイムの核心であり、そのフィールドの変更はランタイム全体の動作に影響を与える可能性があります。- このコミットでは
selgen
が削除されたため、G
構造体の定義を比較することで、その影響をより深く理解できます。
- Goのソースコード内の
- Goのチャネルの内部実装に関する詳細な技術ブログや論文(例: "Go channels are bad and you should not use them" by Peter Bourgon, "Go channels are not bad" by Dave Cheneyなど、議論を深めるための資料)
- これらの資料は、チャネルの設計思想、パフォーマンス特性、および内部的な複雑さについて多角的な視点を提供します。
- このコミットのようなランタイムレベルの最適化が、Goの並行処理モデル全体にどのように貢献しているかを理解するのに役立ちます。
- Goのメモリモデルに関する公式ドキュメント: https://go.dev/ref/mem
- チャネルがどのようにゴルーチン間の同期と通信を保証し、メモリの可視性を確立するかを理解する上で重要です。
- このコミットにおけるアトミック操作の使用は、Goのメモリモデルの原則に沿ったものです。
- Goのコンパイラとリンカの動作に関する情報
static
キーワードがどのようにコンパイルとリンクのプロセスに影響を与えるかを理解する上で役立ちます。- シンボル解決と名前空間の管理に関する知識は、この変更の背景にある設計上の考慮事項を把握するのに重要です。
- Goのレース検出器 (Race Detector) に関する情報
- コミットメッセージに
raceenabled
という記述があるため、Goのレース検出器がチャネル操作の安全性をどのように検証しているかを理解するのに役立ちます。 runtime·racereadobjectpc
やruntime·racewriteobjectpc
といった関数は、レース検出器がメモリアクセスを追跡するために使用するものです。- このコミットでは、これらの関数が
runtime·chansend
やruntime·chanrecv
の代わりに、静的化されたchansend
やchanrecv
を参照するように更新されています。
- コミットメッセージに
これらの情報源は、このコミットの技術的な詳細を深く掘り下げ、Goランタイムの設計思想と進化を理解するための包括的なコンテキストを提供します。