[インデックス 16796] ファイルの概要
このコミットは、Goランタイムにおけるプリエンプション(横取り)メカニズムの信頼性を向上させるための変更です。具体的には、プリエンプションシグナルが失われる可能性があった問題を解決し、より堅牢なプリエンプションを実現しています。
コミット
commit 5887f142a33fbb8da94088e902ced4101a16aa8f
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Jul 17 12:52:37 2013 -0400
runtime: more reliable preemption
Currently preemption signal g->stackguard0==StackPreempt
can be lost if it is received when preemption is disabled
(e.g. m->lock!=0). This change duplicates the preemption
signal in g->preempt and restores g->stackguard0
when preemption is enabled.
Update #543.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/10792043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5887f142a33fbb8da94088e902ced4101a16aa8f
元コミット内容
runtime: more reliable preemption
Currently preemption signal g->stackguard0==StackPreempt
can be lost if it is received when preemption is disabled
(e.g. m->lock!=0). This change duplicates the preemption
signal in g->preempt and restores g->stackguard0
when preemption is enabled.
Update #543.
変更の背景
Goランタイムにおけるプリエンプションは、Goのスケジューラが実行中のゴルーチンを中断し、別のゴルーチンに切り替えることで、公平性と応答性を確保するための重要なメカニズムです。Go 1.14以前は協調的プリエンプションが主であり、ゴルーチンは関数呼び出しなどの「セーフポイント」でのみプリエンプション可能でした。しかし、無限ループなどで関数呼び出しがない場合、そのゴルーチンがCPUを占有し続け、他のゴルーチンが実行されない「ゴルーチン飢餓」が発生する可能性がありました。
このコミットが作成された2013年時点では、Goのプリエンプションはg->stackguard0 == StackPreempt
という特殊なスタックガード値を利用して行われていました。これは、スタックの拡張チェック時にプリエンプションをトリガーする仕組みです。しかし、このシグナルは、プリエンプションが無効になっている期間(例えば、ミューテックスがロックされている間など、m->lock != 0
の場合)に受信されると失われる可能性がありました。プリエンプションが無効な間にシグナルが来ても、そのシグナルが記憶されず、プリエンプションが有効になったときに再度トリガーされないため、プリエンプションが遅延したり、全く行われなかったりする問題がありました。
このコミットは、このプリエンプションシグナルの喪失を防ぎ、より信頼性の高いプリエンプションを実現することを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とプリエンプションの仕組みに関する知識が必要です。
- ゴルーチン (Goroutine): Goランタイムによって管理される軽量な実行スレッドです。Goの並行処理の基本単位となります。各ゴルーチンは
G
構造体で表現されます。 - M (Machine): OSのスレッドに相当します。Goランタイムは、OSスレッド上でゴルーチンを実行します。
M
構造体で表現されます。 - P (Processor): 論理プロセッサに相当し、ゴルーチンを実行するためのコンテキストを提供します。
P
構造体で表現され、M
とG
の間に位置します。 - スケジューラ: Goランタイムの重要なコンポーネントで、ゴルーチンを
M
とP
に割り当て、実行を管理します。 - プリエンプション (Preemption): 実行中のゴルーチンを強制的に中断し、別のゴルーチンにCPUを切り替えるメカニズムです。これにより、特定のゴルーチンがCPUを独占するのを防ぎ、公平なリソース配分を実現します。
g->stackguard0
: 各ゴルーチン(G
構造体)が持つフィールドで、スタックの境界を示す値です。Goランタイムは、関数呼び出し時にスタックがこの境界を超えていないかチェックします。StackPreempt
:g->stackguard0
に設定される特殊な値で、プリエンプションを要求するシグナルとして機能します。スタックチェック時にg->stackguard0
がStackPreempt
であると検出されると、ランタイムはプリエンプション処理を開始します。m->locks
: 現在のM
(OSスレッド)が保持しているランタイムロックの数を示すカウンタです。この値が0でない場合、ランタイムは重要な処理を実行中であり、プリエンプションを一時的に無効にする必要があります。m->mallocing
: 現在のM
がメモリ割り当て中であるかを示すフラグです。メモリ割り当て中はプリエンプションを無効にする必要があります。m->gcing
: 現在のM
がGC処理中であるかを示すフラグです。GC処理中はプリエンプションを無効にする必要があります。runtime·newstack
: スタックの拡張やプリエンプション処理を行うランタイム関数です。g->stackguard0
がStackPreempt
であると検出された場合に呼び出されます。
技術的詳細
このコミットの核心は、プリエンプションシグナルの永続化と復元メカニズムの導入です。
従来のGoランタイムでは、プリエンプションは主にg->stackguard0
フィールドにStackPreempt
という特殊な値を設定することで行われていました。ゴルーチンが関数呼び出しを行う際、ランタイムはg->stackguard0
と現在のスタックポインタを比較し、g->stackguard0
がStackPreempt
であれば、プリエンプションが必要であると判断し、runtime·newstack
関数を呼び出してプリエンプション処理を開始します。
しかし、このメカニズムには問題がありました。runtime·newstack
関数内で、プリエンプションが一時的に無効化される条件(例: m->locks != 0
、m->mallocing != 0
、m->gcing != 0
)がチェックされます。これらの条件が真の場合、つまりランタイムがロックを保持している、メモリ割り当て中である、またはGC中である場合、プリエンプションは即座には行われず、ゴルーチンは実行を継続します。この際、g->stackguard0
は元のg->stackguard
に戻されていました。
問題は、プリエンプションが無効な期間中にStackPreempt
が設定されても、その情報が失われてしまうことでした。プリエンプションが無効な状態が解除された後、ランタイムはプリエンプションが要求されていたことを「忘れて」しまい、そのゴルーチンはプリエンプションされることなく実行を続けてしまう可能性がありました。
このコミットは、この問題を解決するために、G
構造体に新しいフィールドg->preempt
(bool
型)を導入します。
- プリエンプション要求の複製: プリエンプションが要求された際(
runtime·preemptone
関数内)、g->stackguard0 = StackPreempt
を設定するだけでなく、新しく追加されたg->preempt = true
も設定します。これにより、プリエンプション要求がg->stackguard0
とg->preempt
の2箇所に記録されるようになります。 - プリエンプションの遅延と復元:
runtime·newstack
関数内でプリエンプションが一時的に無効化される場合、g->stackguard0
は元の値に戻されますが、g->preempt
はtrue
のまま維持されます。 - プリエンプション有効化時の復元: ランタイムがロックを解放する、メモリ割り当てを終了する、GC処理を終了するなど、プリエンプションが無効化されていた状態が解除されるタイミングで、
m->locks == 0 && g->preempt
という条件をチェックします。この条件が真であれば、以前にプリエンプションが要求されていたことを検出し、g->stackguard0 = StackPreempt
を再度設定します。これにより、ゴルーチンが次にスタックチェックを行う際に、プリエンプションがトリガーされることが保証されます。 - プリエンプション実行後のリセット: 実際にプリエンプションが実行され、ゴルーチンが実行状態(
Grunning
)になった際には、g->preempt = false
にリセットされます。
この変更により、プリエンプションシグナルが一時的に失われることなく、プリエンプションが可能な状態になったときに確実に再トリガーされるようになり、Goランタイムのプリエンプションメカニズムの信頼性が大幅に向上しました。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
src/pkg/runtime/lock_futex.c
src/pkg/runtime/lock_sema.c
src/pkg/runtime/malloc.goc
src/pkg/runtime/mgc0.c
src/pkg/runtime/proc.c
src/pkg/runtime/runtime.h
src/pkg/runtime/stack.c
それぞれのファイルにおける主要な変更点は以下の通りです。
src/pkg/runtime/runtime.h
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -253,6 +253,7 @@ struct G
bool issystem; // do not output in stack dump
bool isbackground; // ignore in deadlock detector
bool blockingsyscall; // hint that the next syscall will block
+ bool preempt; // preemption signal, duplicates stackguard0 = StackPreempt
int8 traceignore; // ignore race detection events
M* m; // for debuggers, but offset not hard-coded
M* lockedm;
G
構造体にpreempt
という新しいbool
フィールドが追加されています。これは、プリエンプション要求が一時的に無効化された場合でも、その要求を記憶するためのフラグです。
src/pkg/runtime/stack.c
--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -250,7 +250,7 @@ runtime·newstack(void)\n \t// We are interested in preempting user Go code, not runtime code.\n \tif(oldstatus != Grunning || m->locks || m->mallocing || m->gcing) {\n \t\t// Let the goroutine keep running for now.\n-\t\t\t// TODO(dvyukov): remember but delay the preemption.\n+\t\t\t// gp->preempt is set, so it will be preempted next time.\n \t\tgp->stackguard0 = gp->stackguard;\n \t\tgp->status = oldstatus;\n \t\truntime·gogo(&gp->sched);\t// never return
runtime·newstack
関数内で、プリエンプションが一時的に無効化される条件(m->locks
、m->mallocing
、m->gcing
)が満たされた場合、gp->stackguard0
は元の値に戻されますが、gp->preempt
が設定されているため、プリエンプション要求が記憶されることがコメントで示されています。
--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -2174,6 +2187,7 @@ if(1) return;\n gp = mp->curg;\n if(gp == nil || gp == mp->g0)\n return;\n+ gp->preempt = true;\n gp->stackguard0 = StackPreempt;\n }\n \n```
`runtime·preemptone`関数(プリエンプションを要求する関数)内で、`gp->stackguard0 = StackPreempt`を設定するだけでなく、`gp->preempt = true`も設定されるようになりました。
### `src/pkg/runtime/lock_futex.c`, `src/pkg/runtime/lock_sema.c`, `src/pkg/runtime/malloc.goc`, `src/pkg/runtime/mgc0.c`, `src/pkg/runtime/proc.c`
これらのファイルでは、ランタイムロックの解放、メモリ割り当ての終了、GC処理の終了など、プリエンプションが無効化されていた状態が解除される可能性のある箇所で、以下のコードが追加されています。
```diff
--- a/src/pkg/runtime/lock_futex.c
+++ b/src/pkg/runtime/lock_futex.c
@@ -99,6 +100,8 @@ runtime·unlock(Lock *l)\n \n if(--m->locks < 0)\n runtime·throw("runtime·unlock: lock count");
+ if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
+ g->stackguard0 = StackPreempt;
}
(lock_futex.c
とlock_sema.c
のruntime·unlock
関数内)
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -94,6 +95,8 @@ runtime·mallocgc(uintptr size, uint32 flag, int32 dogc, int32 zeroed)\n *(uintptr*)((uintptr)v+size-sizeof(uintptr)) = 0;\n \n m->mallocing = 0;
+ if(g->preempt) // restore the preemption request in case we've cleared it in newstack
+ g->stackguard0 = StackPreempt;
\n if(!(flag & FlagNoProfiling) && (rate = runtime·MemProfileRate) > 0) {
(malloc.goc
のruntime·mallocgc
関数内)
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -2031,6 +2032,8 @@ runtime·gc(int32 force)\n // give the queued finalizers, if any, a chance to run\n runtime·gosched();
}
+ if(g->preempt) // restore the preemption request in case we've cleared it in newstack
+ g->stackguard0 = StackPreempt;
}
(mgc0.c
のruntime·gc
関数内)
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -294,6 +294,8 @@ runtime·ready(G *gp)\n if(runtime·atomicload(&runtime·sched.npidle) != 0 && runtime·atomicload(&runtime·sched.nmspinning) == 0) // TODO: fast atomic\n wakep();
m->locks--;
+ if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
+ g->stackguard0 = StackPreempt;
}
(proc.c
のruntime·ready
, runtime·starttheworld
, runtime·allocm
, runtime·exitsyscall
, runtime·newproc1
関数内)
これらの箇所では、m->locks == 0 && g->preempt
という条件がチェックされ、もしプリエンプションが無効化されていた期間にプリエンプション要求があった場合(g->preempt
がtrue
)、g->stackguard0
をStackPreempt
に再設定しています。
また、src/pkg/runtime/proc.c
のexecute
関数では、ゴルーチンが実行状態になる際にgp->preempt = false
にリセットされています。
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1008,6 +1014,7 @@ execute(G *gp)\n runtime·throw("execute: bad g status");
}
gp->status = Grunning;
+ gp->preempt = false;
gp->stackguard0 = gp->stackguard;
m->p->tick++;
m->curg = gp;
コアとなるコードの解説
このコミットの主要な変更は、G
構造体にpreempt
フィールドを追加し、プリエンプション要求のライフサイクルをより堅牢に管理することです。
-
G
構造体へのpreempt
フィールド追加:src/pkg/runtime/runtime.h
でbool preempt;
がG
構造体に追加されました。これは、プリエンプション要求が発行されたが、ランタイムの内部状態(例: ロック保持中)により即座に処理できない場合に、その要求を記憶するためのフラグです。 -
プリエンプション要求時の
preempt
設定:src/pkg/runtime/stack.c
のruntime·preemptone
関数は、特定のゴルーチンをプリエンプション対象としてマークする役割を担います。この関数内で、従来のgp->stackguard0 = StackPreempt;
に加えて、gp->preempt = true;
が追加されました。これにより、プリエンプション要求がstackguard0
とpreempt
の両方に記録されることになります。 -
runtime·newstack
でのプリエンプション遅延処理の改善:src/pkg/runtime/stack.c
のruntime·newstack
関数は、スタックの拡張が必要な場合やプリエンプションが要求された場合に呼び出されます。この関数内で、m->locks
、m->mallocing
、m->gcing
などのフラグがチェックされ、ランタイムが重要な内部処理を行っている間はプリエンプションを一時的に無効化します。 変更前は、この無効化期間中にstackguard0
が元の値に戻されると、プリエンプション要求が失われていました。変更後は、gp->preempt
がtrue
のまま維持されるため、プリエンプション要求が記憶されます。コメント// gp->preempt is set, so it will be preempted next time.
が追加され、この新しい動作が明確にされています。 -
プリエンプション有効化時の
stackguard0
復元:src/pkg/runtime/lock_futex.c
,src/pkg/runtime/lock_sema.c
(runtime·unlock
関数),src/pkg/runtime/malloc.goc
(runtime·mallocgc
関数),src/pkg/runtime/mgc0.c
(runtime·gc
関数),src/pkg/runtime/proc.c
(runtime·ready
,runtime·starttheworld
,runtime·allocm
,runtime·exitsyscall
,runtime·newproc1
関数) など、ランタイムがロックを解放したり、内部処理を完了したりしてプリエンプションが再び可能になる可能性のある多くの箇所で、以下のパターンが追加されました。if(m->locks == 0 && g->preempt) g->stackguard0 = StackPreempt;
このコードは、
m->locks
が0(ランタイムロックが解放された状態)であり、かつg->preempt
がtrue
(以前にプリエンプション要求があった)の場合に、g->stackguard0
をStackPreempt
に再設定します。これにより、ゴルーチンが次にスタックチェックを行う際に、プリエンプションが確実にトリガーされるようになります。これは、プリエンプション要求が一時的に遅延されたとしても、最終的に実行されることを保証する重要なメカニズムです。 -
プリエンプション実行後の
preempt
リセット:src/pkg/runtime/proc.c
のexecute
関数は、ゴルーチンが実際に実行状態(Grunning
)になる直前に呼び出されます。この関数内でgp->preempt = false;
が追加されました。これは、プリエンプションが正常に処理された後、preempt
フラグをリセットし、次のプリエンプション要求に備えるためです。
これらの変更により、Goランタイムはプリエンプション要求をより確実に処理できるようになり、プリエンプションが無効な期間中に発生した要求も失われることなく、適切なタイミングで実行されるようになりました。これにより、スケジューラの公平性が向上し、ゴルーチン飢餓の発生が抑制されます。
関連リンク
- Go CL: https://golang.org/cl/10792043
- Go Issue #543: https://github.com/golang/go/issues/543 (このコミットが解決する問題に関連するIssue)
参考にした情報源リンク
- Go runtime preemption: https://dzone.com/articles/go-runtime-preemption
- Go runtime preemption: https://unskilled.blog/go-runtime-preemption/
- Go runtime preemption: https://www.sap.com/documents/2023/09/92231220-427c-0010-b286-fd51a8201a1e.html
- Go runtime preemption: https://go.googlesource.com/go/+/refs/heads/master/src/runtime/preempt.go
- Go runtime preemption: https://medium.com/@ankur_anand/go-scheduler-part-2-preemption-in-go-1-14-a7020830a2b
- Go runtime preemption: https://medium.com/@ankur_anand/go-scheduler-part-1-cooperative-preemption-in-go-1-13-and-earlier-a7020830a2b
- Go runtime preemption: https://go.dev/blog/go1.14-preemption
- Go runtime preemption: https://stackoverflow.com/questions/60000000/how-does-go-1-14-preemption-work
- Go runtime preemption: https://medium.com/@ankur_anand/go-scheduler-part-3-asynchronous-preemption-in-go-1-14-a7020830a2b
- Go runtime preemption: https://stackoverflow.com/questions/60000000/how-does-go-1-14-preemption-work