Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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ごとに異なり、コードの重複や最適化の機会損失が生じていたという問題がありました。具体的には、以下の点が挙げられます。

  1. コードの重複: 各OS(Linux, Windows, Darwin, FreeBSD, OpenBSD, Plan 9など)のthread.cファイル内に、それぞれ独自のミューテックス実装が存在していました。これは、OS固有の同期プリミティブ(futex、セマフォ、イベントなど)を直接利用していたためです。
  2. 最適化の限定: LinuxやWindowsでは、より効率的な同期メカニズム(futexなど)が利用可能でしたが、他のOSではそれらの最適化が適用されていませんでした。これにより、OS間のパフォーマンスにばらつきが生じていました。
  3. チャネルファイナライザの課題: Goのチャネルは、そのライフサイクル管理においてファイナライザ(runtime.addfinalizer)を使用している場合がありました。ファイナライザはガベージコレクション(GC)と連携してリソースを解放する仕組みですが、その実行タイミングが不確定であることや、GCのオーバーヘッドを増大させる可能性があるため、パフォーマンス上のボトルネックとなることがありました。このコミットでは、ミューテックス実装の改善により、チャネルのファイナライザを不要にすることが可能になりました。
  4. 将来的な最適化の基盤: ミューテックス実装を共通化することで、将来的に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つの実装に集約したことです。

  1. 汎用ミューテックス実装の導入:

    • src/pkg/runtime/lock_futex.c: LinuxやFreeBSDのようにfutexシステムコールをサポートするOS向けの汎用ミューテックス実装です。このファイルには、runtime·lockruntime·unlockruntime·noteclearruntime·notewakeupruntime·notesleepといった関数が定義されています。これらの関数は、内部的にruntime·futexsleepruntime·futexwakeupというOS固有のコールバック関数を利用します。
    • src/pkg/runtime/lock_sema.c: Darwin (macOS)、OpenBSD、Plan 9、Windowsのようにセマフォやイベントベースの同期プリミティブを使用するOS向けの汎用ミューテックス実装です。このファイルも同様に、runtime·lockruntime·unlockruntime·noteclearruntime·notewakeupruntime·notesleepといった関数を定義し、内部的にruntime·semacreateruntime·semasleepruntime·semawakeupというOS固有のコールバック関数を利用します。
  2. OS固有コードの簡素化:

    • 各OSのsrc/pkg/runtime/<os>/thread.cファイルから、ミューテックスや通知(Note)に関する具体的な実装が削除されました。
    • 代わりに、これらのファイルは、新しく導入された汎用ミューテックス実装が要求する少数のOS固有のコールバック関数(例: runtime·futexsleep, runtime·futexwakeup for futex-based; runtime·semacreate, runtime·semasleep, runtime·semawakeup for semaphore-based)を提供するようになりました。これにより、OS固有のコード量が大幅に削減され、保守性が向上しました。
  3. runtime.hの変更:

    • LockNote構造体の定義が変更され、OS固有のフィールドが削除され、汎用的なkey(futex用)とwaitm(セマフォ用)の共用体(union)として再定義されました。これにより、異なるOSで同じ構造体定義を使用できるようになりました。
    • Usema構造体が削除されました。これは、セマフォベースの汎用実装が導入されたため、ユーザー空間セマフォの個別実装が不要になったためです。
  4. チャネルファイナライザの廃止:

    • src/pkg/runtime/chan.cから、チャネルのファイナライザ(runtime·addfinalizer(c, (void*)destroychan, 0);およびdestroychan関数)が削除されました。これは、ミューテックス実装の改善により、チャネルのロック管理がより効率的になり、ファイナライザによるリソース解放が不要になったことを示唆しています。ファイナライザの廃止は、GCのオーバーヘッド削減とパフォーマンス向上に寄与します。
  5. パフォーマンスベンチマーク:

    • コミットメッセージには、Linux/amd64とDarwin/amd64におけるベンチマーク結果が示されています。
    • BenchmarkChanContendedは、チャネルの競合が多いシナリオでのパフォーマンスを示しており、Linuxでは改善が見られます。
    • Darwinでは、BenchmarkChanContendedのシングルコアでの性能はわずかに悪化していますが、マルチコア(-2, -4)での性能は劇的に改善しています。これは、セマフォベースの新しい実装がマルチコア環境での競合解決をより効率的に行えるようになったためと考えられます。
    • BenchmarkChanCreationもDarwinで大幅に改善しており、チャネルの作成コストが削減されたことを示しています。

この変更により、Goランタイムはよりモジュール化され、OS間のコードの共通化が進み、将来的なパフォーマンス改善のための強固な基盤が構築されました。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。

  1. src/pkg/runtime/Makefile:

    • 新しく追加されたlock_futex.clock_sema.cが、各OSのビルドプロセスに組み込まれるように変更されました。
    • OFILES_darwin, OFILES_freebsd, OFILES_linux, OFILES_openbsd, OFILES_plan9, OFILES_windowsといったOS固有のオブジェクトファイルリストに、適切なロック実装のオブジェクトファイルが追加されています。
  2. src/pkg/runtime/chan.c:

    • チャネルのファイナライザに関連するコード(runtime·addfinalizer(c, (void*)destroychan, 0);destroychan関数)が削除されました。
  3. src/pkg/runtime/darwin/thread.csrc/pkg/runtime/freebsd/thread.csrc/pkg/runtime/linux/thread.csrc/pkg/runtime/openbsd/thread.csrc/pkg/runtime/plan9/thread.csrc/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)が定義されるようになりました。
  4. src/pkg/runtime/lock_futex.c (新規追加):

    • futexシステムコールを利用するOS向けの汎用ミューテックス実装が含まれています。
    • runtime·lock, runtime·unlock, runtime·noteclear, runtime·notewakeup, runtime·notesleepといった関数が定義されています。
  5. src/pkg/runtime/lock_sema.c (新規追加):

    • セマフォやイベントを利用するOS向けの汎用ミューテックス実装が含まれています。
    • runtime·lock, runtime·unlock, runtime·noteclear, runtime·notewakeup, runtime·notesleepといった関数が定義されています。
  6. src/pkg/runtime/runtime.h:

    • LockNote構造体の定義が、OS固有のフィールドを持たない汎用的な共用体(union)として変更されました。
    • Usema構造体が削除されました。
    • M構造体に、セマフォベースのロック実装で使用されるnextwaitm, waitsema, waitsemacount, waitsemalockといったフィールドが追加されました。

これらの変更により、Goランタイムの同期プリミティブのアーキテクチャが大きく再構築されました。

コアとなるコードの解説

このコミットの核心は、lock_futex.clock_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ベースのロックと同様にfutexsleepfutexwakeupを利用して、スレッドの待機と通知を行います。

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_SPINruntime·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->waitmLOCKEDであれば、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を利用して、スレッドの待機と通知を行います。

OS固有のthread.cファイルにおけるコールバックの実装

各OSのthread.cファイルは、上記の汎用ロック実装が呼び出す具体的なOS固有の関数を実装しています。

  • Linux (src/pkg/runtime/linux/thread.c):

    • runtime·futexsleepfutex(FUTEX_WAIT)システムコールを呼び出します。
    • runtime·futexwakeupfutex(FUTEX_WAKE)システムコールを呼び出します。
  • Darwin (src/pkg/runtime/darwin/thread.c):

    • runtime·semacreateruntime·mach_semcreate()(Machカーネルのセマフォ作成)を呼び出します。
    • runtime·semasleepruntime·mach_semacquire()(Machカーネルのセマフォ取得)を呼び出します。
    • runtime·semawakeupruntime·mach_semrelease()(Machカーネルのセマフォ解放)を呼び出します。

このように、汎用的なロックロジックはlock_futex.clock_sema.cに集約され、OS固有の低レベルなシステムコール呼び出しは各OSのthread.cファイルにカプセル化されることで、コードのモジュール化と再利用性が大幅に向上しています。

関連リンク

参考にした情報源リンク