[インデックス 10206] ファイルの概要
このコミットは、Goランタイムにおけるミューテックス(mutex)の実装をOS間で統一することを目的としています。これにより、コードの重複が削減され、LinuxやWindowsでのみ利用可能だった最適化が他のOSにも拡張され、さらなる最適化の基盤が提供されました。特に、チャネルのファイナライザが最終的に廃止された点が重要な変更点として挙げられます。
コミット
commit ee24bfc0584f368284c2a4bef8e54056876677e9
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Nov 2 16:42:01 2011 +0300
runtime: unify mutex code across OSes
The change introduces 2 generic mutex implementations
(futex- and semaphore-based). Each OS chooses a suitable mutex
implementation and implements few callbacks (e.g. futex wait/wake).
The CL reduces code duplication, extends some optimizations available
only on Linux/Windows to other OSes and provides ground
for futher optimizations. Chan finalizers are finally eliminated.
(Linux/amd64, 8 HT cores)
benchmark old new
BenchmarkChanContended 83.6 77.8 ns/op
BenchmarkChanContended-2 341 328 ns/op
BenchmarkChanContended-4 382 383 ns/op
BenchmarkChanContended-8 390 374 ns/op
BenchmarkChanContended-16 313 291 ns/op
(Darwin/amd64, 2 cores)
benchmark old new
BenchmarkChanContended 159 172 ns/op
BenchmarkChanContended-2 6735 263 ns/op
BenchmarkChanContended-4 10384 255 ns/op
BenchmarkChanCreation 1174 407 ns/op
BenchmarkChanCreation-2 4007 254 ns/op
BenchmarkChanCreation-4 4029 246 ns/op
R=rsc, jsing, hectorchu
CC=golang-dev
https://golang.org/cl/5140043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ee24bfc0584f368284c2a4bef8e54056876677e9
元コミット内容
runtime: unify mutex code across OSes
The change introduces 2 generic mutex implementations
(futex- and semaphore-based). Each OS chooses a suitable mutex
implementation and implements few callbacks (e.g. futex wait/wake).
The CL reduces code duplication, extends some optimizations available
only on Linux/Windows to other OSes and provides ground
for futher optimizations. Chan finalizers are finally eliminated.
(Linux/amd64, 8 HT cores)
benchmark old new
BenchmarkChanContended 83.6 77.8 ns/op
BenchmarkChanContended-2 341 328 ns/op
BenchmarkChanContended-4 382 383 ns/op
BenchmarkChanContended-8 390 374 ns/op
BenchmarkChanContended-16 313 291 ns/op
(Darwin/amd64, 2 cores)
benchmark old new
BenchmarkChanContended 159 172 ns/op
BenchmarkChanContended-2 6735 263 ns/op
BenchmarkChanContended-4 10384 255 ns/op
BenchmarkChanCreation 1174 407 ns/op
BenchmarkChanCreation-2 4007 254 ns/op
BenchmarkChanCreation-4 4029 246 ns/op
R=rsc, jsing, hectorchu
CC=golang-dev
https://golang.org/cl/5140043
変更の背景
このコミットが行われた背景には、Goランタイムにおけるミューテックスの実装がOSごとに異なり、コードの重複や最適化の機会損失が生じていたという問題がありました。具体的には、以下の点が挙げられます。
- コードの重複: 各OS(Linux, Windows, Darwin, FreeBSD, OpenBSD, Plan 9など)の
thread.cファイル内に、それぞれ独自のミューテックス実装が存在していました。これは、OS固有の同期プリミティブ(futex、セマフォ、イベントなど)を直接利用していたためです。 - 最適化の限定: LinuxやWindowsでは、より効率的な同期メカニズム(futexなど)が利用可能でしたが、他のOSではそれらの最適化が適用されていませんでした。これにより、OS間のパフォーマンスにばらつきが生じていました。
- チャネルファイナライザの課題: Goのチャネルは、そのライフサイクル管理においてファイナライザ(
runtime.addfinalizer)を使用している場合がありました。ファイナライザはガベージコレクション(GC)と連携してリソースを解放する仕組みですが、その実行タイミングが不確定であることや、GCのオーバーヘッドを増大させる可能性があるため、パフォーマンス上のボトルネックとなることがありました。このコミットでは、ミューテックス実装の改善により、チャネルのファイナライザを不要にすることが可能になりました。 - 将来的な最適化の基盤: ミューテックス実装を共通化することで、将来的にGoランタイム全体の同期メカニズムに対するさらなる最適化(例えば、より高度なスピンロック戦略や、ユーザー空間でのロック競合解決の改善など)を容易にする基盤を築くことが目的でした。
これらの課題を解決するため、OS固有のミューテックス実装を汎用的な2つの実装(futexベースとセマフォベース)に集約し、OSはそれぞれの実装が要求する少数のコールバック(例: futex wait/wake)を提供する形に再構築されました。
前提知識の解説
このコミットを理解するためには、以下の概念について理解しておく必要があります。
1. Goランタイム (Go Runtime)
Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。Goランタイムは、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ管理、同期プリミティブ(ミューテックス、チャネルなど)といった低レベルな機能を提供します。これらの機能は、Goプログラムの並行性とパフォーマンスを支える基盤となります。
2. ゴルーチン (Goroutine)
ゴルーチンはGoにおける軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量であり、数百万のゴルーチンを同時に実行することも可能です。ゴルーチンはGoランタイムのスケジューラによって管理され、OSスレッドにマッピングされて実行されます。
3. チャネル (Channel)
チャネルは、ゴルーチン間で値を送受信するためのGoの同期プリミティブです。チャネルを使用することで、ゴルーチン間の安全な通信と同期を実現できます。チャネルは内部的にロック(ミューテックス)を使用して、複数のゴルーチンからの同時アクセスを制御しています。
4. ミューテックス (Mutex)
ミューテックス(Mutual Exclusionの略)は、複数のスレッド(またはゴルーチン)が共有リソースに同時にアクセスするのを防ぐための同期プリミティブです。ミューテックスは、共有リソースへのアクセスを排他的に制御し、データ競合を防ぎます。Goではsync.Mutexとして提供されていますが、その低レベルな実装はGoランタイム内に存在します。
5. ファイナライザ (Finalizer)
ファイナライザは、オブジェクトがガベージコレクションによってメモリから解放される直前に実行される関数です。Goではruntime.SetFinalizer関数を使って設定できます。主に、Goのヒープ外で確保されたリソース(C言語のライブラリが確保したメモリ、ファイルディスクリプタ、ネットワークソケットなど)を解放するために使用されます。しかし、ファイナライザの実行タイミングはGCに依存するため不確定であり、パフォーマンス上のオーバーヘッドを招く可能性があります。
6. Futex (Fast Userspace Mutex)
Futex(Fast Userspace Mutex)は、Linuxカーネルが提供する同期プリミティブです。ユーザー空間でロックの競合が発生しない限りカーネルモードへの切り替えを避けることで、高速なミューテックス実装を可能にします。競合が発生した場合のみ、カーネルのfutex()システムコールを呼び出してスレッドをスリープさせたり、ウェイクアップさせたりします。これにより、コンテキストスイッチのオーバーヘッドを最小限に抑え、高いパフォーマンスを実現します。
7. セマフォ (Semaphore)
セマフォは、複数のプロセスやスレッドが共有リソースにアクセスする際の同期を制御するための抽象データ型です。セマフォはカウンタを持ち、wait(P操作、カウンタをデクリメントし、0ならブロック)とsignal(V操作、カウンタをインクリメントし、ブロックされているスレッドをウェイクアップ)の2つの操作で制御されます。ミューテックスはバイナリセマフォ(カウンタが0または1)の一種と考えることができます。OSによっては、カーネルレベルのセマフォが提供されており、スレッドのブロックとウェイクアップに使用されます。
8. スピンロック (Spinlock)
スピンロックは、ロックが解放されるのを待つ間、スレッドがCPUを占有し続ける(スピンし続ける)ロックメカニズムです。ロックが短時間で解放されることが予想される場合に有効ですが、ロックが長時間保持されるとCPU時間を無駄に消費するため、効率が悪くなります。このコミットでは、ミューテックス実装において、最初に短いスピンロックを試み、それでもロックが取得できない場合にOSの同期プリミティブ(futexやセマフォ)にフォールバックするハイブリッドなアプローチが採用されています。
技術的詳細
このコミットの主要な技術的変更点は、Goランタイムのミューテックス実装をOS固有のコードから汎用的な2つの実装に集約したことです。
-
汎用ミューテックス実装の導入:
src/pkg/runtime/lock_futex.c: LinuxやFreeBSDのようにfutexシステムコールをサポートするOS向けの汎用ミューテックス実装です。このファイルには、runtime·lock、runtime·unlock、runtime·noteclear、runtime·notewakeup、runtime·notesleepといった関数が定義されています。これらの関数は、内部的にruntime·futexsleepとruntime·futexwakeupというOS固有のコールバック関数を利用します。src/pkg/runtime/lock_sema.c: Darwin (macOS)、OpenBSD、Plan 9、Windowsのようにセマフォやイベントベースの同期プリミティブを使用するOS向けの汎用ミューテックス実装です。このファイルも同様に、runtime·lock、runtime·unlock、runtime·noteclear、runtime·notewakeup、runtime·notesleepといった関数を定義し、内部的にruntime·semacreate、runtime·semasleep、runtime·semawakeupというOS固有のコールバック関数を利用します。
-
OS固有コードの簡素化:
- 各OSの
src/pkg/runtime/<os>/thread.cファイルから、ミューテックスや通知(Note)に関する具体的な実装が削除されました。 - 代わりに、これらのファイルは、新しく導入された汎用ミューテックス実装が要求する少数のOS固有のコールバック関数(例:
runtime·futexsleep,runtime·futexwakeupfor futex-based;runtime·semacreate,runtime·semasleep,runtime·semawakeupfor semaphore-based)を提供するようになりました。これにより、OS固有のコード量が大幅に削減され、保守性が向上しました。
- 各OSの
-
runtime.hの変更:LockとNote構造体の定義が変更され、OS固有のフィールドが削除され、汎用的なkey(futex用)とwaitm(セマフォ用)の共用体(union)として再定義されました。これにより、異なるOSで同じ構造体定義を使用できるようになりました。Usema構造体が削除されました。これは、セマフォベースの汎用実装が導入されたため、ユーザー空間セマフォの個別実装が不要になったためです。
-
チャネルファイナライザの廃止:
src/pkg/runtime/chan.cから、チャネルのファイナライザ(runtime·addfinalizer(c, (void*)destroychan, 0);およびdestroychan関数)が削除されました。これは、ミューテックス実装の改善により、チャネルのロック管理がより効率的になり、ファイナライザによるリソース解放が不要になったことを示唆しています。ファイナライザの廃止は、GCのオーバーヘッド削減とパフォーマンス向上に寄与します。
-
パフォーマンスベンチマーク:
- コミットメッセージには、Linux/amd64とDarwin/amd64におけるベンチマーク結果が示されています。
BenchmarkChanContendedは、チャネルの競合が多いシナリオでのパフォーマンスを示しており、Linuxでは改善が見られます。- Darwinでは、
BenchmarkChanContendedのシングルコアでの性能はわずかに悪化していますが、マルチコア(-2, -4)での性能は劇的に改善しています。これは、セマフォベースの新しい実装がマルチコア環境での競合解決をより効率的に行えるようになったためと考えられます。 BenchmarkChanCreationもDarwinで大幅に改善しており、チャネルの作成コストが削減されたことを示しています。
この変更により、Goランタイムはよりモジュール化され、OS間のコードの共通化が進み、将来的なパフォーマンス改善のための強固な基盤が構築されました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
-
src/pkg/runtime/Makefile:- 新しく追加された
lock_futex.cとlock_sema.cが、各OSのビルドプロセスに組み込まれるように変更されました。 OFILES_darwin,OFILES_freebsd,OFILES_linux,OFILES_openbsd,OFILES_plan9,OFILES_windowsといったOS固有のオブジェクトファイルリストに、適切なロック実装のオブジェクトファイルが追加されています。
- 新しく追加された
-
src/pkg/runtime/chan.c:- チャネルのファイナライザに関連するコード(
runtime·addfinalizer(c, (void*)destroychan, 0);とdestroychan関数)が削除されました。
- チャネルのファイナライザに関連するコード(
-
src/pkg/runtime/darwin/thread.c、src/pkg/runtime/freebsd/thread.c、src/pkg/runtime/linux/thread.c、src/pkg/runtime/openbsd/thread.c、src/pkg/runtime/plan9/thread.c、src/pkg/runtime/windows/thread.c:- これらのOS固有の
thread.cファイルから、ミューテックス(runtime·lock,runtime·unlock)と通知(runtime·noteclear,runtime·notesleep,runtime·notewakeup)の具体的な実装が削除されました。 - 代わりに、新しい汎用ロック実装が利用するOS固有のコールバック関数(例:
runtime·futexsleep,runtime·futexwakeup,runtime·semacreate,runtime·semasleep,runtime·semawakeup)が定義されるようになりました。
- これらのOS固有の
-
src/pkg/runtime/lock_futex.c(新規追加):futexシステムコールを利用するOS向けの汎用ミューテックス実装が含まれています。runtime·lock,runtime·unlock,runtime·noteclear,runtime·notewakeup,runtime·notesleepといった関数が定義されています。
-
src/pkg/runtime/lock_sema.c(新規追加):- セマフォやイベントを利用するOS向けの汎用ミューテックス実装が含まれています。
runtime·lock,runtime·unlock,runtime·noteclear,runtime·notewakeup,runtime·notesleepといった関数が定義されています。
-
src/pkg/runtime/runtime.h:LockとNote構造体の定義が、OS固有のフィールドを持たない汎用的な共用体(union)として変更されました。Usema構造体が削除されました。M構造体に、セマフォベースのロック実装で使用されるnextwaitm,waitsema,waitsemacount,waitsemalockといったフィールドが追加されました。
これらの変更により、Goランタイムの同期プリミティブのアーキテクチャが大きく再構築されました。
コアとなるコードの解説
このコミットの核心は、lock_futex.cとlock_sema.cに導入された汎用的なロック実装と、それらがOS固有のコールバック関数をどのように利用するかという点にあります。
src/pkg/runtime/lock_futex.c の解説
このファイルは、LinuxやFreeBSDなどのfutexシステムコールをサポートするOSで使用されるミューテックス実装を提供します。
-
runtime·lock(Lock *l):- まず、
m->locks++で現在のM(Machine、OSスレッドに相当)が保持するロック数をインクリメントします。これはデバッグやエラーチェックのためです。 runtime·xchg(&l->key, MUTEX_LOCKED)で、ロックのキーをMUTEX_LOCKEDに設定し、以前の値をアトミックに取得します。もし以前がMUTEX_UNLOCKEDであれば、ロックを即座に取得できたことになります。- ロックが取得できなかった場合(競合が発生した場合)、スピンロックを試みます。
runtime·ncpu > 1の場合、ACTIVE_SPIN回(30回)のループでruntime·procyield(CPUを一時的に解放するが、すぐに再スケジュールされる可能性のある命令)を呼び出し、ロックが解放されるのを待ちます。 - スピンロックでも取得できない場合、
PASSIVE_SPIN回(1回)のループでruntime·osyield(OSにCPUを明け渡す)を呼び出し、他のスレッドに実行を譲ります。 - それでもロックが取得できない場合、
runtime·xchg(&l->key, MUTEX_SLEEPING)でロックの状態をMUTEX_SLEEPINGに設定し、runtime·futexsleep(&l->key, MUTEX_SLEEPING)を呼び出してスレッドをスリープさせます。runtime·futexsleepはOS固有のコールバック関数であり、Linuxではfutex(FUTEX_WAIT)システムコールを呼び出します。
- まず、
-
runtime·unlock(Lock *l):m->locks--でロック数をデクリメントします。runtime·xchg(&l->key, MUTEX_UNLOCKED)でロックのキーをMUTEX_UNLOCKEDに設定し、以前の値をアトミックに取得します。- もし以前の状態が
MUTEX_SLEEPINGであれば、runtime·futexwakeup(&l->key, 1)を呼び出して、スリープしているスレッドを1つウェイクアップさせます。runtime·futexwakeupはOS固有のコールバック関数であり、Linuxではfutex(FUTEX_WAKE)システムコールを呼び出します。
-
runtime·noteclear,runtime·notewakeup,runtime·notesleep:- これらの関数は、Goランタイムの通知(Note)メカニズムを実装しており、内部的に
futexベースのロックと同様にfutexsleepとfutexwakeupを利用して、スレッドの待機と通知を行います。
- これらの関数は、Goランタイムの通知(Note)メカニズムを実装しており、内部的に
src/pkg/runtime/lock_sema.c の解説
このファイルは、Darwin、OpenBSD、Plan 9、Windowsなどのセマフォやイベントベースの同期プリミティブを使用するOSで使用されるミューテックス実装を提供します。
-
runtime·lock(Lock *l):m->locks++でロック数をインクリメントします。runtime·casp(&l->waitm, nil, (void*)LOCKED)で、l->waitm(待機中のMのリンクリストの先頭)がnilであれば、それをLOCKEDに設定してロックを取得します。これは、競合がない場合の高速パスです。- ロックが取得できなかった場合、現在のM(
m)に紐付けられたセマフォ(m->waitsema)がまだ作成されていなければ、runtime·semacreate()を呼び出して作成します。runtime·semacreateはOS固有のコールバック関数であり、例えばDarwinではmach_semcreateを呼び出します。 - スピンロックを試みます。
runtime·ncpu > 1の場合、ACTIVE_SPIN回スピンし、その後PASSIVE_SPIN回runtime·osyieldを呼び出します。 - それでもロックが取得できない場合、現在のMを待機中のMのリンクリスト(
l->waitm)にアトミックに追加し、runtime·semasleep()を呼び出してスレッドをスリープさせます。runtime·semasleepはOS固有のコールバック関数であり、例えばDarwinではmach_semacquireを呼び出します。
-
runtime·unlock(Lock *l):m->locks--でロック数をデクリメントします。runtime·atomicloadp(&l->waitm)で待機中のMのリストの先頭を取得します。- もし
l->waitmがLOCKEDであれば、runtime·casp(&l->waitm, (void*)LOCKED, nil)でロックを解放します。 - もし待機中のMが存在する場合、リンクリストからMを1つデキューし、そのMのセマフォに対して
runtime·semawakeup(mp)を呼び出してウェイクアップさせます。runtime·semawakeupはOS固有のコールバック関数であり、例えばDarwinではmach_semreleaseを呼び出します。
-
runtime·noteclear,runtime·notewakeup,runtime·notesleep:- これらの関数も、Goランタイムの通知メカニズムを実装しており、内部的にセマフォベースのロックと同様に
semacreate,semasleep,semawakeupを利用して、スレッドの待機と通知を行います。
- これらの関数も、Goランタイムの通知メカニズムを実装しており、内部的にセマフォベースのロックと同様に
OS固有のthread.cファイルにおけるコールバックの実装
各OSのthread.cファイルは、上記の汎用ロック実装が呼び出す具体的なOS固有の関数を実装しています。
-
Linux (
src/pkg/runtime/linux/thread.c):runtime·futexsleepはfutex(FUTEX_WAIT)システムコールを呼び出します。runtime·futexwakeupはfutex(FUTEX_WAKE)システムコールを呼び出します。
-
Darwin (
src/pkg/runtime/darwin/thread.c):runtime·semacreateはruntime·mach_semcreate()(Machカーネルのセマフォ作成)を呼び出します。runtime·semasleepはruntime·mach_semacquire()(Machカーネルのセマフォ取得)を呼び出します。runtime·semawakeupはruntime·mach_semrelease()(Machカーネルのセマフォ解放)を呼び出します。
このように、汎用的なロックロジックはlock_futex.cとlock_sema.cに集約され、OS固有の低レベルなシステムコール呼び出しは各OSのthread.cファイルにカプセル化されることで、コードのモジュール化と再利用性が大幅に向上しています。
関連リンク
- Go Change List: https://golang.org/cl/5140043
参考にした情報源リンク
- Linux futex(2) - Linux man page
- Go の runtime.SetFinalizer の使い方と注意点 - Qiita (一般的なファイナライザの概念理解のため)
- Go言語の並行処理と同期プリミティブ - Qiita (Goの並行処理と同期プリミティブの一般的な理解のため)
- GoのruntimeパッケージのLockとUnlock - Qiita (Goランタイムのロックに関する一般的な理解のため)
- セマフォ (コンピュータ) - Wikipedia (セマフォの一般的な概念理解のため)
- スピンロック - Wikipedia (スピンロックの一般的な概念理解のため)