[インデックス 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ランタイムの設計思想と進化を理解するための包括的なコンテキストを提供します。