[インデックス 1571] ファイルの概要
このコミットは、Go言語のランタイムにおける複数の競合状態(race conditions)を修正することを目的としています。具体的には、スケジューラの停止ロジック、ロックカウンタの管理、およびLinux環境でのfutexベースのロック機構の利用方法に関する改善が含まれています。
コミット
commit 53e69e1db5d5960b33c93e05236afaca7f110b2b
Author: Russ Cox <rsc@golang.org>
Date: Tue Jan 27 14:01:20 2009 -0800
various race conditions.
R=r
DELTA=43 (29 added, 5 deleted, 9 changed)
OCL=23608
CL=23611
---
src/runtime/proc.c | 6 +++++-\
src/runtime/rt1_amd64_darwin.c | 8 +++++++-\
src/runtime/rt1_amd64_linux.c | 38 ++++++++++++++++++++++++++------------
3 files changed, 38 insertions(+), 14 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/53e69e1db5d5960b33c93e05236afaca7f110b2b
元コミット内容
このコミットは、Goランタイムの以下のファイルに影響を与えています。
src/runtime/proc.c
: スケジューラのプロセス管理に関するコード。src/runtime/rt1_amd64_darwin.c
: macOS (Darwin) 上のAMD64アーキテクチャ向けランタイムコード。src/runtime/rt1_amd64_linux.c
: Linux上のAMD64アーキテクチャ向けランタイムコード。
主な変更点は以下の通りです。
src/runtime/proc.c
において、スケジューラの停止条件にsched.mcpu <= sched.mcpumax
という条件が追加されました。これは、CPU使用率が最大値を超えていない場合にのみスケジューラの停止を考慮するというものです。src/runtime/rt1_amd64_darwin.c
およびsrc/runtime/rt1_amd64_linux.c
において、lock
およびunlock
関数内でm->locks
というロックカウンタのインクリメント/デクリメントのロジックが修正され、負の値になった場合にthrow("lock count")
でパニックを起こすようになりました。これにより、ロックの不適切な解放(例えば、ロックされていないミューテックスを解放しようとする)を検出できるようになります。src/runtime/rt1_amd64_linux.c
において、lock
とunlock
の内部実装がfutexlock
とfutexunlock
という静的関数に分離され、Note
オブジェクト(Goランタイムの内部的な通知メカニズム)が直接futexlock
/futexunlock
を使用するように変更されました。これにより、Note
の内部ロックがm->locks
カウンタに影響を与えないようになり、より正確なロック管理が可能になります。
変更の背景
このコミットが行われた2009年当時、Go言語はまだ開発の初期段階にあり、ランタイムの安定性とパフォーマンスの向上が重要な課題でした。特に、並行処理を扱うGoにおいて、競合状態はプログラムの予期せぬ動作やデッドロックの原因となるため、その修正は優先度の高いタスクでした。
このコミットの背景には、以下のような問題意識があったと考えられます。
- スケジューラの競合状態:
sched.waitstop
のようなスケジューラ関連のフラグが、複数のゴルーチンやM(OSスレッド)によって同時にアクセスされる際に、不正確な状態遷移を引き起こす可能性がありました。特に、CPUリソースの管理と連携する際に、デッドロックや非効率なスケジューリングが発生するリスクがありました。 - ロックカウンタの不整合:
m->locks
は、現在のM(OSスレッド)が保持しているロックの数を追跡するためのカウンタです。このカウンタが正しく管理されていないと、デバッグが困難なロック関連のバグ(例えば、ロックの二重解放や、ロックされていないミューテックスの解放)が発生する可能性があります。初期のGoランタイムでは、このカウンタの更新タイミングやエラーチェックが不十分であった可能性があります。 - futexの適切な利用: Linuxのfutex(Fast Userspace muTEX)は、ユーザー空間での効率的な同期プリミティブを提供しますが、その利用は非常に低レベルであり、正確な実装が求められます。
Note
オブジェクトのようなランタイム内部のプリミティブがfutexを直接利用する際に、m->locks
のような高レベルのロックカウンタのセマンティクスと混同されると、意図しない副作用が生じる可能性がありました。このコミットは、Note
のロックがMのロックカウンタとは独立して機能するように分離することで、この問題を解決しようとしています。
これらの修正は、Goランタイムの堅牢性を高め、より安定した並行処理環境を提供するための重要なステップでした。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とOSの同期プリミティブに関する知識が必要です。
Goランタイムの基本構造
Goランタイムは、Goプログラムの実行を管理するC言語で書かれた部分です。主な構成要素は以下の通りです。
- G (Goroutine): Go言語の軽量スレッド。Goプログラムの並行実行の単位です。
- M (Machine): OSスレッド。Gを実行するための実際のOSスレッドです。
- P (Processor): 論理プロセッサ。MとGを関連付けるコンテキストを提供します。Pの数は通常、CPUコアの数に等しく、MがGを実行するためにPを必要とします。
スケジューラ (sched
)
Goランタイムには、GをM上で実行するためのスケジューラがあります。sched
構造体は、スケジューラのグローバルな状態を保持します。
sched.waitstop
: スケジューラが停止を待っている状態を示すフラグ。sched.mcpu
: 現在のCPU使用率に関連する値、または利用可能なCPUの数。sched.mcpumax
: 利用可能なCPUの最大数。noteclear
,notewakeup
,notesleep
: Goランタイム内部で使われる、一回限りの通知(one-time notification)メカニズム。特定のイベントが発生した際に、待機しているゴルーチンを起床させるために使用されます。これらは低レベルの同期プリミティブです。
ロック (Lock
構造体と m->locks
)
Goランタイムは、内部的な同期のために独自のロックメカニズムを使用します。
Lock
構造体: ランタイム内部で使用されるミューテックスのようなロックプリミティブ。m->locks
: 各M(OSスレッド)に紐付けられたカウンタで、そのMが現在保持しているランタイム内部ロックの数を追跡します。これは、デバッグやランタイムの健全性チェックのために使用されます。例えば、ロックを解放する際にこのカウンタが0未満になると、それはロックされていないものを解放しようとしていることを意味し、バグの可能性を示唆します。
Futex (Fast Userspace muTEX)
FutexはLinuxカーネルが提供する低レベルの同期プリミティブです。ユーザー空間のプログラムが、カーネルの介入を最小限に抑えながら効率的に同期を行うことを可能にします。
- ユーザー空間での高速パス: 競合がない場合、futexはユーザー空間でアトミック操作のみでロック/アンロックを完了できます。
- カーネルへのフォールバック: 競合が発生した場合、カーネルにシステムコールを発行して、スレッドをスリープさせたり、ウェイクアップさせたりします。
futexlock
,futexunlock
: このコミットで導入された、futexを直接利用する低レベルのロック/アンロック関数。
競合状態 (Race Condition)
複数のスレッドやゴルーチンが共有リソースに同時にアクセスし、そのアクセス順序によって結果が変わる可能性がある状況を指します。競合状態は、プログラムの非決定的な動作やバグの原因となります。
技術的詳細
src/runtime/proc.c
の変更
nextgandunlock
関数と sys·entersyscall
関数において、if(sched.waitstop)
の条件が if(sched.waitstop && sched.mcpu <= sched.mcpumax)
に変更されました。
nextgandunlock
: この関数は、現在のMが次に実行するゴルーチンを選択し、スケジューラのロックを解放する際に呼び出されます。sched.waitstop
は、スケジューラが停止状態にあることを示しますが、sched.mcpu <= sched.mcpumax
の追加は、システムがCPUリソースの最大利用可能数を超えていない場合にのみ、スケジューラの停止処理(notewakeup(&sched.stopped)
)を行うべきであることを示唆しています。これは、スケジューラが不必要に停止状態に入ったり、CPUリソースがまだ利用可能であるにもかかわらず停止処理がトリガーされたりする競合状態を防ぐためと考えられます。sys·entersyscall
: システムコールに入る際に呼び出される関数です。ここでも同様の条件が追加されています。システムコール中にスケジューラの停止処理が誤ってトリガーされるのを防ぐことで、ランタイムの安定性を向上させます。
これらの変更は、スケジューラの停止ロジックが、CPUリソースの利用状況とより密接に連携するように調整され、不正確な停止状態への遷移を防ぐことを目的としています。
src/runtime/rt1_amd64_darwin.c
および src/runtime/rt1_amd64_linux.c
の変更
両方のファイルで、lock
および unlock
関数における m->locks
カウンタの管理が改善されました。
lock
関数:m->locks++
の行がxadd(&l->key, 1)
の呼び出しの前に移動されました。これにより、ロックを取得しようとする試みが始まる前に、Mがロックを保持しようとしている意図がカウンタに反映されます。これは、ロック取得がブロックされる可能性がある場合でも、カウンタが常に正確な状態を反映するようにするために重要です。unlock
関数:m->locks--
の行がxadd(&l->key, -1)
の呼び出しの前に移動されました。また、if(m->locks < 0) throw("lock count");
というチェックが追加されました。これは、ロックが解放される前にカウンタがデクリメントされ、その結果カウンタが負の値になった場合に、ロックの不適切な解放(例えば、二重解放や、ロックされていないミューテックスの解放)を即座に検出してパニックを発生させるためのものです。これにより、デバッグが困難なロック関連のバグを早期に発見できます。
src/runtime/rt1_amd64_linux.c
の追加変更
Linux固有のランタイムコードでは、futexベースのロック実装に関するより大きな変更が行われました。
futexlock
とfutexunlock
の導入: 既存のlock
とunlock
関数の実体が、それぞれstatic void futexlock(Lock *l)
とstatic void futexunlock(Lock *l)
という名前の静的関数にリファクタリングされました。これにより、これらの関数はファイルスコープに限定され、外部からは直接呼び出されなくなります。- 新しい
lock
とunlock
ラッパー: 新たにvoid lock(Lock *l)
とvoid unlock(Lock *l)
関数が定義されました。これらの関数は、m->locks
カウンタの管理(インクリメント/デクリメントと負の値チェック)を行い、その後で実際のロック/アンロック処理をfutexlock
/futexunlock
に委譲します。 Note
オブジェクトの変更:noteclear
,notewakeup
,notesleep
関数が、内部のロック (n->lock
) を操作する際に、新しいfutexlock
とfutexunlock
を直接使用するように変更されました。これは非常に重要な変更です。Note
オブジェクトはGoランタイムの内部的な通知メカニズムであり、そのロックはMが保持する一般的なロックとは異なるセマンティクスを持つべきです。m->locks
カウンタは、ゴルーチンがユーザーレベルで取得するロックや、Mが特定のタスクのために保持するロックを追跡するためのものです。Note
の内部ロックがm->locks
に影響を与えないようにすることで、ロックカウンタの正確性が保たれ、Note
の利用が他のロック操作と干渉しないようになります。特にnotesleep
では、futexlock
とfutexunlock
が連続して呼び出されており、これは「ロックを取得してすぐに解放する」というパターンで、他のスレッドが待機している場合にウェイクアップさせるための一般的な手法です。
これらの変更は、Linux環境におけるGoランタイムのロックメカニズムをよりモジュール化し、m->locks
カウンタの目的を明確にし、Note
オブジェクトのような特殊な同期プリミティブが正しく機能するようにするためのものです。
コアとなるコードの変更箇所
src/runtime/proc.c
--- a/src/runtime/proc.c
+++ b/src/runtime/proc.c
@@ -397,7 +397,7 @@ nextgandunlock(void)
throw("all goroutines are asleep - deadlock!");
m->nextg = nil;
noteclear(&m->havenextg);
- if(sched.waitstop) {
+ if(sched.waitstop && sched.mcpu <= sched.mcpumax) {
sched.waitstop = 0;
notewakeup(&sched.stopped);
}
@@ -590,6 +590,10 @@ sys·entersyscall(uint64 callerpc, int64 trap)
sched.msyscall++;
if(sched.gwait != 0)
matchmg();
+ if(sched.waitstop && sched.mcpu <= sched.mcpumax) {
+ sched.waitstop = 0;
+ notewakeup(&sched.stopped);
+ }
unlock(&sched);
// leave SP around for gc; poison PC to make sure it's not used
g->sched.SP = (byte*)&callerpc;
src/runtime/rt1_amd64_darwin.c
--- a/src/runtime/rt1_amd64_darwin.c
+++ b/src/runtime/rt1_amd64_darwin.c
@@ -277,19 +277,25 @@ xadd(uint32 volatile *val, int32 delta)
void
lock(Lock *l)
{
+\tif(m->locks < 0)
+\t\tthrow("lock count");
+\tm->locks++;
+\n // Allocate semaphore if needed.
if(l->sema == 0)
initsema(&l->sema);
if(xadd(&l->key, 1) > 1) // someone else has it; wait
mach_semacquire(l->sema);
-\tm->locks++;
}
void
unlock(Lock *l)
{
m->locks--;
+\tif(m->locks < 0)
+\t\tthrow("lock count");
+\n if(xadd(&l->key, -1) > 0) // someone else is waiting
mach_semrelease(l->sema);
}
src/runtime/rt1_amd64_linux.c
--- a/src/runtime/rt1_amd64_linux.c
+++ b/src/runtime/rt1_amd64_linux.c
@@ -301,13 +301,11 @@ futexwakeup(uint32 *addr)
// else return 0;
// but atomically.
-void
-lock(Lock *l)
+static void
+futexlock(Lock *l)
{
uint32 v;
-\tm->locks++;
-\n again:
\tv = l->key;
\tif((v&1) == 0){
@@ -346,13 +344,11 @@ again:
\tgoto again;\n }\n \n-void\n-unlock(Lock *l)\n+static void\n+futexunlock(Lock *l)\n {\n uint32 v;\n \n-\tm->locks--;
-\n // Atomically get value and clear lock bit.
again:
\tv = l->key;
@@ -366,6 +362,24 @@ again:
\t\tfutexwakeup(&l->key);\n }\n \n+void\n+lock(Lock *l)\n+{\n+\tif(m->locks < 0)\n+\t\tthrow("lock count");\n+\tm->locks++;\n+\tfutexlock(l);\n+}\n+\n+void\n+unlock(Lock *l)\n+{\n+\tm->locks--;\n+\tif(m->locks < 0)\n+\t\tthrow("lock count");\n+\tfutexunlock(l);\n+}\n+\n \n // One-time notifications.\n //\n@@ -383,20 +397,20 @@ void\n noteclear(Note *n)\n {\n \tn->lock.key = 0;\t// memset(n, 0, sizeof *n)\n-\tlock(&n->lock);\n+\tfutexlock(&n->lock);\n }\n \n void\n notewakeup(Note *n)\n {\n-\tunlock(&n->lock);\n+\tfutexunlock(&n->lock);\n }\n \n void\n notesleep(Note *n)\n {\n-\tlock(&n->lock);\n-\tunlock(&n->lock);\t// Let other sleepers find out too.\n+\tfutexlock(&n->lock);\n+\tfutexunlock(&n->lock);\t// Let other sleepers find out too.\n }\n \n \n```
## コアとなるコードの解説
### `src/runtime/proc.c` の変更点
- **`nextgandunlock` と `sys·entersyscall` における `sched.waitstop` の条件強化**:
- 変更前: `if(sched.waitstop)`
- 変更後: `if(sched.waitstop && sched.mcpu <= sched.mcpumax)`
- この変更は、スケジューラが停止状態にあると判断されるだけでなく、現在のCPU使用率 (`sched.mcpu`) が最大利用可能CPU数 (`sched.mcpumax`) を超えていない場合にのみ、スケジューラの停止関連の処理(`notewakeup(&sched.stopped)`)を実行するようにします。これにより、CPUリソースがまだ利用可能であるにもかかわらず、スケジューラが誤って停止状態に陥る可能性のある競合状態を防ぎます。これは、システムが過負荷状態にあるときに不必要な停止処理を避け、より堅牢なスケジューリングを実現するためのものです。
### `src/runtime/rt1_amd64_darwin.c` および `src/runtime/rt1_amd64_linux.c` の変更点
- **`lock` 関数における `m->locks++` の移動**:
- 変更前: `xadd(&l->key, 1)` の後に `m->locks++`
- 変更後: `xadd(&l->key, 1)` の前に `m->locks++`
- `m->locks` は、現在のM(OSスレッド)が保持しているランタイム内部ロックの数を追跡するカウンタです。ロックを取得する試み (`xadd(&l->key, 1)`) が始まる前にカウンタをインクリメントすることで、ロック取得がブロックされた場合でも、Mがロックを取得しようとしている状態が正確に反映されるようになります。これにより、ロックの取得が完了する前にMが別の処理に切り替わった場合でも、`m->locks` の値が常にMのロック保持意図を正確に表すようになります。
- **`unlock` 関数における `m->locks--` の移動と負の値チェック**:
- 変更前: `xadd(&l->key, -1)` の後に `m->locks--`
- 変更後: `xadd(&l->key, -1)` の前に `m->locks--` と `if(m->locks < 0) throw("lock count");` の追加
- ロックを解放する前にカウンタをデクリメントすることで、ロック解放処理が完了する前にカウンタが更新されます。
- `if(m->locks < 0) throw("lock count");` の追加は非常に重要です。これは、`m->locks` が負の値になった場合に即座にパニックを発生させます。`m->locks` が負になるのは、ロックされていないミューテックスを解放しようとした場合や、ロックが二重に解放された場合など、ロック管理に深刻なバグがあることを意味します。このチェックにより、このようなバグを早期に検出し、デバッグを容易にします。
### `src/runtime/rt1_amd64_linux.c` の追加変更点
- **`futexlock` と `futexunlock` の導入と `lock`/`unlock` のラッパー化**:
- 既存の `lock` と `unlock` の実体が `static void futexlock(Lock *l)` と `static void futexunlock(Lock *l)` にリファクタリングされました。これらの関数は、Linuxのfutexシステムコールを直接利用した低レベルのロック/アンロック操作を行います。
- 新たに定義された `void lock(Lock *l)` と `void unlock(Lock *l)` は、`m->locks` カウンタの管理(インクリメント/デクリメントと負の値チェック)を行い、その後で `futexlock` / `futexunlock` を呼び出します。これにより、`m->locks` の管理と実際のfutex操作が分離され、コードのモジュール性が向上します。
- **`Note` オブジェクトのロック操作の変更**:
- `noteclear`, `notewakeup`, `notesleep` 関数内で、`n->lock` を操作する際に、従来の `lock(&n->lock)` / `unlock(&n->lock)` ではなく、直接 `futexlock(&n->lock)` / `futexunlock(&n->lock)` を使用するように変更されました。
- この変更の意図は、`Note` オブジェクトの内部ロックが、Mの一般的なロックカウンタ (`m->locks`) の影響を受けないようにすることです。`Note` はランタイム内部の特定の通知メカニズムであり、そのロックはMが保持する他のロックとは異なるセマンティクスを持つべきです。`futexlock`/`futexunlock` を直接使用することで、`Note` のロック操作が `m->locks` のインクリメント/デクリメントをトリガーせず、ランタイム全体のロックカウンタの正確性を維持しつつ、`Note` の機能が独立して動作することを保証します。特に `notesleep` で `futexlock` と `futexunlock` が連続して呼び出されるのは、待機しているスレッドをウェイクアップさせるための一般的なパターンであり、この場合も `m->locks` を不必要に操作しないことが重要です。
これらの変更は、Goランタイムの初期段階における並行処理のバグを修正し、より堅牢で正確な同期メカニズムを構築するための重要なステップでした。
## 関連リンク
- Go言語の公式ドキュメント: [https://go.dev/doc/](https://go.dev/doc/)
- Goランタイムのソースコード (GitHub): [https://github.com/golang/go/tree/master/src/runtime](https://github.com/golang/go/tree/master/src/runtime)
- Linux Futexes (Wikipedia): [https://en.wikipedia.org/wiki/Futex](https://en.wikipedia.org/wiki/Futex)
## 参考にした情報源リンク
- Go言語の初期のコミット履歴 (GitHub): [https://github.com/golang/go/commits/master](https://github.com/golang/go/commits/master)
- Go言語のランタイムに関する議論や設計ドキュメント (Go Wikiなど、当時の情報源を探す必要あり)
- Linuxカーネルのfutexに関するドキュメントや解説記事
- Go言語の並行処理モデルに関する書籍や論文 (当時のGoの設計思想を理解するため)
- Russ Coxのブログや発表資料 (Goの初期開発に関する洞察を得るため)
(注: 2009年当時のGoランタイムの内部実装に関する詳細な公開資料は限られているため、上記の「参考にした情報源リンク」は一般的な情報源を示しています。実際の分析は、コミットのコード変更とGoランタイムの一般的な知識に基づいています。)