[インデックス 15394] ファイルの概要
このコミットは、Goランタイムにおけるローカルワークキューの実装を追加するものです。これは、新しいスケジューラ導入に向けた準備の一環として行われました。具体的には、src/pkg/runtime/proc.c
にゴルーチンを管理するためのローカルキュー操作関数(runqput
, runqget
, runqgrow
, runqsteal
)が追加され、関連するヘッダファイルsrc/pkg/runtime/runtime.h
にP
構造体が定義されました。また、これらの機能のテストコードがsrc/pkg/runtime/export_test.go
とsrc/pkg/runtime/proc_test.go
に追加されています。
コミット
- コミットハッシュ:
353ce60f6e8e5898efec62945fbafd33bc28a37b
- Author: Dmitriy Vyukov dvyukov@google.com
- Date: Sat Feb 23 08:48:02 2013 +0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/353ce60f6e8e5898efec62945fbafd33bc28a37b
元コミット内容
runtime: implement local work queues (in preparation for new scheduler)
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7402047
変更の背景
このコミットは、Goランタイムのスケジューラを刷新する準備として、ローカルワークキューを導入するものです。Goのスケジューラは、ゴルーチン(Goの軽量スレッド)を効率的にOSスレッド(M)とプロセッサ(P)に割り当て、実行を管理する役割を担っています。従来のスケジューラでは、ゴルーチンの管理にグローバルなキューが用いられていましたが、マルチコア環境でのスケーラビリティやパフォーマンスの向上のためには、各プロセッサが自身のゴルーチンキューを持つ「ローカルワークキュー」の概念が不可欠となります。
特に、この変更は「ワークスチール(Work Stealing)」アルゴリズムの実装に向けた重要なステップです。ワークスチールとは、あるプロセッサ(P)が自身のローカルキューに実行可能なゴルーチンがない場合に、他のプロセッサのローカルキューからゴルーチンを「盗んで」きて実行するメカニズムです。これにより、CPUコアの利用率を最大化し、負荷分散を最適化することができます。このコミットは、そのワークスチールを実現するための基盤となるローカルキューの管理機能を提供します。
前提知識の解説
Goのランタイムスケジューラを理解するためには、以下の概念が重要です。
- Goroutine (G): Go言語における軽量な実行単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することが可能です。Goランタイムがそのスケジューリングを管理します。
- Machine (M): OSスレッドを表します。Goのコードを実行する実際のOSスレッドです。MはP(Processor)にアタッチされてゴルーチンを実行します。
- Processor (P): 論理的なプロセッサを表します。Goランタイムがゴルーチンを実行するために必要なコンテキストを提供します。通常、
GOMAXPROCS
環境変数で指定された数(デフォルトではCPUコア数)のPが存在します。各Pは自身のローカルな実行可能ゴルーチンキュー(Local Run Queue, LRQ)を持ちます。 - Local Run Queue (LRQ): 各Pが持つ、実行可能なゴルーチンを格納するキューです。Pはまず自身のLRQからゴルーチンを取得して実行します。
- Global Run Queue (GRQ): 全てのPが共有するグローバルな実行可能ゴルーチンキューです。新しいゴルーチンは通常、最初にGRQに配置されるか、LRQが満杯になった場合にLRQからGRQに移動されることがあります。
- Work Stealing (ワークスチール): Goスケジューラの重要なメカニズムの一つです。あるPのLRQが空になった場合、そのPはアイドル状態になるのではなく、他のPのLRQからゴルーチンを「盗んで」きて自身のLRQに追加し、実行を継続します。これにより、CPUリソースの有効活用と負荷分散が実現されます。
このコミットは、特にLRQの実装と、ワークスチールに必要なrunqsteal
関数の基礎を築いています。
技術的詳細
このコミットでは、Goランタイムのproc.c
ファイルに、P(Processor)ごとのローカルな実行可能ゴルーチンキューを管理するための関数群が追加されています。
-
P
構造体の導入:src/pkg/runtime/runtime.h
にP
構造体が定義されました。この構造体は、ローカルキューのヘッド、テール、サイズ、そしてゴルーチンへのポインタの配列(runq
)を保持します。また、キューへのアクセスを保護するためのLock
も含まれています。struct P { Lock; // Queue of runnable goroutines. G** runq; int32 runqhead; int32 runqtail; int32 runqsize; };
-
ローカルキュー操作関数:
src/pkg/runtime/proc.c
に以下の関数が追加されました。runqput(P *p, G *gp)
: 指定されたPのローカルキューにゴルーチンgp
を追加します。キューが満杯の場合にはrunqgrow
を呼び出してキューを拡張します。キューへのアクセスはロックで保護されています。runqget(P *p)
: 指定されたPのローカルキューからゴルーチンを取得します。キューが空の場合にはnil
を返します。キューへのアクセスはロックで保護されています。runqgrow(P *p)
: ローカルキューが満杯になった際に、キューのサイズを2倍に拡張します。既存のゴルーチンは新しい、より大きな配列にコピーされます。runqsteal(P *p, P *p2)
: ワークスチールの中核となる関数です。Pp
がPp2
のローカルキューからゴルーチンを「盗みます」。具体的には、p2
のキューから約半分のゴルーチンをp
のキューに移動させます。デッドロックを防ぐために、ロックの取得順序が考慮されています(p < p2
の場合とそうでない場合でロック順序を制御)。盗んだゴルーチンのうちの1つを返します。
これらの関数は、Goスケジューラが各Pのローカルキューを効率的に管理し、必要に応じて他のPからゴルーチンを奪い取ることで、システム全体の負荷分散とスループット向上を図るための基盤となります。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルと、その変更の概要は以下の通りです。
src/pkg/runtime/export_test.go
:testSchedLocalQueue()
とtestSchedLocalQueueSteal()
というテスト用の関数がエクスポートされ、外部のテストコードから呼び出せるようになりました。これにより、ローカルキューの基本的な動作とワークスチール機能のテストが可能になります。
src/pkg/runtime/proc.c
:- ローカルワークキューを操作するための主要な関数である
runqput
、runqget
、runqgrow
、runqsteal
が実装されました。 - これらの関数は、P(プロセッサ)ごとのゴルーチンキューの管理、キューの拡張、そして他のPからのゴルーチン窃盗(ワークスチール)のロジックを含んでいます。
- また、これらの関数をテストするための
runtime·testSchedLocalQueue
とruntime·testSchedLocalQueueSteal
というテストヘルパー関数も追加されています。
- ローカルワークキューを操作するための主要な関数である
src/pkg/runtime/proc_test.go
:TestSchedLocalQueue
とTestSchedLocalQueueSteal
というGoのテスト関数が追加されました。これらは、export_test.go
でエクスポートされたテストヘルパー関数を呼び出し、ローカルキューとワークスチール機能の正確性を検証します。
src/pkg/runtime/runtime.h
:P
構造体が定義されました。この構造体は、各プロセッサが持つローカルな実行可能ゴルーチンキューの状態(ヘッド、テール、サイズ、ゴルーチン配列)を保持します。
コアとなるコードの解説
src/pkg/runtime/proc.c
に追加された主要な関数について詳しく解説します。
static void runqput(P *p, G *gp)
この関数は、指定されたプロセッサp
のローカル実行可能キューにゴルーチンgp
を追加します。
static void
runqput(P *p, G *gp)
{
int32 h, t, s;
runtime·lock(p); // Pのロックを取得し、排他制御を行う
retry:
h = p->runqhead; // キューのヘッド
t = p->runqtail; // キューのテール
s = p->runqsize; // キューの現在のサイズ
if(t == h-1 || (h == 0 && t == s-1)) { // キューが満杯の場合
runqgrow(p); // キューを拡張
goto retry; // 拡張後、再度追加を試みる
}
p->runq[t++] = gp; // テール位置にゴルーチンを追加し、テールをインクリメント
if(t == s) // テールがキューの終端に達した場合
t = 0; // テールを先頭に戻す(リングバッファとして利用)
p->runqtail = t; // 更新されたテールをPに設定
runtime·unlock(p); // Pのロックを解放
}
この関数はリングバッファとして実装されており、runqhead
とrunqtail
がキューの先頭と末尾を示します。キューが満杯の場合(t == h-1
またはh == 0 && t == s-1
)、runqgrow
を呼び出してキューのサイズを倍に拡張します。
static G* runqget(P *p)
この関数は、指定されたプロセッサp
のローカル実行可能キューからゴルーチンを取得します。
static G*
runqget(P *p)
{
G *gp;
int32 t, h, s;
if(p->runqhead == p->runqtail) // キューが空の場合
return nil; // nilを返す
runtime·lock(p); // Pのロックを取得
h = p->runqhead;
t = p->runqtail;
s = p->runqsize;
if(t == h) { // ロック取得後にキューが空になった場合
runtime·unlock(p);
return nil;
}
gp = p->runq[h++]; // ヘッド位置のゴルーチンを取得し、ヘッドをインクリメント
if(h == s) // ヘッドがキューの終端に達した場合
h = 0; // ヘッドを先頭に戻す
p->runqhead = h; // 更新されたヘッドをPに設定
runtime·unlock(p); // Pのロックを解放
return gp; // 取得したゴルーチンを返す
}
runqput
と同様にリングバッファとして動作します。キューが空の場合はnil
を返します。
static void runqgrow(P *p)
この関数は、ローカル実行可能キューのサイズを拡張します。
static void
runqgrow(P *p)
{
G **q;
int32 s, t, h, t2;
h = p->runqhead;
t = p->runqtail;
s = p->runqsize;
t2 = 0;
q = runtime·malloc(2*s*sizeof(*q)); // 現在の2倍のサイズの新しい配列を割り当て
while(t != h) { // 既存のゴルーチンを新しい配列にコピー
q[t2++] = p->runq[h++];
if(h == s)
h = 0;
}
runtime·free(p->runq); // 古い配列を解放
p->runq = q; // 新しい配列をPに設定
p->runqhead = 0; // ヘッドを0にリセット
p->runqtail = t2; // テールをコピーされた要素数に設定
p->runqsize = 2*s; // サイズを2倍に更新
}
キューが満杯になった際に呼び出され、キューのサイズを倍に増やします。既存のゴルーチンは新しいメモリ領域にコピーされます。
static G* runqsteal(P *p, P *p2)
この関数は、ワークスチールアルゴリズムの中核をなすもので、プロセッサp
がプロセッサp2
のローカルキューからゴルーチンを「盗む」処理を行います。
static G*
runqsteal(P *p, P *p2)
{
G *gp, *gp1;
int32 t, h, s, t2, h2, s2, c, i;
if(p2->runqhead == p2->runqtail) // 盗む側のキューが空の場合
return nil; // nilを返す
// sort locks to prevent deadlocks
if(p < p2) // デッドロックを防ぐために、Pのアドレスに基づいてロック順序を決定
runtime·lock(p);
runtime·lock(p2); // 盗む側のPのロックを取得
if(p2->runqhead == p2->runqtail) { // ロック取得後にキューが空になった場合
runtime·unlock(p2);
if(p < p2)
runtime·unlock(p);
return nil;
}
if(p >= p2) // デッドロックを防ぐために、Pのアドレスに基づいてロック順序を決定
runtime·lock(p);
// now we've locked both queues and know the victim is not empty
h = p->runqhead;
t = p->runqtail;
s = p->runqsize;
h2 = p2->runqhead;
t2 = p2->runqtail;
s2 = p2->runqsize;
gp = p2->runq[h2++]; // 盗むゴルーチンのうちの1つを最初に取得(返り値となる)
if(h2 == s2)
h2 = 0;
// steal roughly half
if(t2 > h2) // 盗む側のキューの約半分を計算
c = (t2 - h2) / 2;
else
c = (s2 - h2 + t2) / 2;
// copy
for(i = 0; i != c; i++) { // 計算した半分のゴルーチンをコピー
// the target queue is full?
if(t == h-1 || (h == 0 && t == s-1)) // 盗む側のキューが満杯の場合
break;
// the victim queue is empty?
if(t2 == h2) // 盗まれる側のキューが空になった場合
break;
gp1 = p2->runq[h2++]; // 盗まれる側のキューからゴルーチンを取得
if(h2 == s2)
h2 = 0;
p->runq[t++] = gp1; // 盗む側のキューに追加
if(t == s)
t = 0;
}
p->runqtail = t; // 盗む側のPのテールを更新
p2->runqhead = h2; // 盗まれる側のPのヘッドを更新
runtime·unlock(p2); // 盗まれる側のPのロックを解放
runtime·unlock(p); // 盗む側のPのロックを解放
return gp; // 最初に盗んだゴルーチンを返す
}
この関数は、p2
のローカルキューから約半分のゴルーチンをp
のローカルキューに移動させます。重要なのは、デッドロックを避けるために、2つのPのロックを常に同じ順序で取得するロジック(if(p < p2)
)が導入されている点です。これにより、複数のPが同時に互いのキューからゴルーチンを盗もうとした場合に発生しうるデッドロックを防ぎます。
関連リンク
参考にした情報源リンク
- GitHub Commit: https://github.com/golang/go/commit/353ce60f6e8e5898efec62945fbafd33bc28a37b
- Go scheduler work stealing (Web Search Results):
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHIPqUZ3ta194EWlEgVRbNoVMHkSvMARpjdvOCtUQO4xa8x5w9c4V9L9qpco5tr3ijmVAJdRZNwaJ7ODGP1zFJCM83-dmWr8IXxyFrMqTEcaBfBCfBxGtnEe06_ppBT8wyMfyvegqnAR5szSyxm5MTp4Yhe3jzYYqsIfh2NdFOoFAaiB-3g-Hje
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQH8UEzglW_RGyf6xvvpMYoZZUFTyPaV6qnOkXI63S2_7AYswhlW5x8ET8-wlNQrDbjOSsCHh97ydN5aK3qffvIjATLPzhebnyG9qOakCm3y7I3RD8Ajwo_8
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHO3Z6SC1Ym1DzaHKQtMUId4kE3-Vjt0WeO69i4uhfPsGepuAww6_-VZXpBrttZyCimaiqdQkynXKs5M9YxBVWc_tgHA1BbQyjB9grZ62Y78_LeTgo1t4bPZP2gVcXzMhBYm7gHojUa0lx69rd6_4oDUCY7lc_R
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEL_oN6K3VW8bsuMgpBrMJjSa4VUqV7OK7zYKbR2aO21myGg9VxV9oBI0ksBg2oZ2HW7Nstpuw73lkOfno6XFwdjHpCVPnTtK2TtqhpIL2vg-3NriGAC8DKsuOXIYoZzQEDQaMaOfs_tybTHFw6o5PwMopIA5uiRwqOmEjODcJUiNuhjg==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEmXIvk9dtArUuqT7VCxW0ZDPBeDdbnX2yBIaWGlW0Dr4vOwYnMXdipX-vezd0_Q57lWNzciRoZGFbKjZfDInsT-vpo5LMPXxWxQGzfmJxwqBnG0_Y7WSHZcpYS53mzvVNxBx8bTu3ri1wwW1EBYnxgLFVRJxLhNtejC_vfal3rNJNdoUx4WXshbA8TcftwTpGV1lFg5EamogEN-pd1J_TLYtOskIXB4hFumZguGjF_yVnBsqT-AqgbeNDASD03vMv
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHYdIo2OaS40i62WzErBz22pPhYNIqCWaXixZPWYdcLWDljUU0Q6iLm5kRvBAN0Dg6ESb0_CE6dkRjQ2apWCnJMgWexm3RhOYYHRxiJVH_tc5s_apFC0_ihdlZGsfCep2DBgn2d5RnPxSWRBkI-WJ2qcSFVc7mYuawu78boBmYYa-k=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEKfXx7W5OFnt-_IU2vYk8dcgLjJ_RVRLbz3Xw0u7RGfJEFDFtce8Bd_oHIsZZp7iaSpNifVlGry5FMdcWMBGftddI3JIIb3HSbw3hxGXiBYvVoO2o46eICjSpkEI4=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHvPVYGsKazVMwjPcLJ0HcmAHyaC2ve2E127DJ5Rm0B_sDwhszSOzlmXZ7n_p_5tFrhIj_Ixmx6k2j6EdVq_ubyHhSAa3m_ckPfR-M2RHqYf7YPto4uDJmD9DDcCyDv_S2fvLElGMbjjme8PoVO_LehqlCNOFfyWIUi2EKwJ50izq_0ygOmIJHNtSUWgA==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQENhCUnzBUN_5Y-7wsdnL24y3gvz5NQa89VUE1oKT65GFMAQIK_lipbdVIodT-cxiYWIDYPuiXQXqrbLldIKNlRHyRF7fGzpKqb9aPooxw-MY0jiYboGDXgTFBR9drTZjKnfPozrrlzvaJi0C_lWAP0WJ3ecz5oZnFXE9CSdg==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEeoAxMcGp2dIOvOsSq7RAVDdwq0g4-6Atrpj1x2wPyU-4Z78qRETD0gU5lPJeIKB-1aSvuZYiN-hwRvC4O0n0oK5YwYUXWh4w7h9koVJf2QR6s35J1-vnkXX4R0XyQyu8BC2zs39kDI3nqadK0-emTH9JFZn3y2alVNkfyo1fMxoLRan1uU_cMTcEs6gw=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFYrc2wen_AI3i2Q9PdpHosPW2Ijg3fc82UHgy0VUiBSDtSdGCN96EZ-QsJMAv78YtehO3-apoIADG2tBRIm0y3gF5xOFnRc-0_615wsJ0Qi31ARwEyBy02yTv3GR6GossMeJivTEhsELwpMyY0qre1mPsReBFvUXB126YfEFQLBxux3HPv6z1ayvb87QkUx9RPdqLbjZ5I3miSIVi6byVAxm8uqgHB