[インデックス 1705] ファイルの概要
このコミットは、Go言語のメモリモデルに関する公式ドキュメント doc/mem.html
を新規追加するものです。このドキュメントは、複数のGoroutine間で共有される変数への読み書きが、どのような条件下で互いに可視となるかを明確に定義することを目的としています。特に、並行処理におけるデータ競合や予期せぬ動作を防ぐための「happens before」関係、およびチャネル、ロック、sync.Once
といった同期プリミティブの振る舞いについて詳細に解説しています。
コミット
- コミットハッシュ:
82c38cf8dd628e6c90b6f1160be2a8d5088b77c9
- Author: Russ Cox rsc@golang.org
- Date: Fri Feb 20 15:35:20 2009 -0800
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/82c38cf8dd628e6c90b6f1160be2a8d5088b77c9
元コミット内容
draft of memory model.
R=tgs
DELTA=545 (545 added, 0 deleted, 0 changed)
OCL=25212
CL=25268
変更の背景
Go言語は、Goroutineとチャネルを言語レベルでサポートすることで、並行処理を容易に記述できることを特徴としています。しかし、複数のGoroutineが共有メモリにアクセスする場合、コンパイラやプロセッサによる命令の再配置(reordering)が発生する可能性があり、これにより予期せぬ動作やデータ競合(data race)が生じることがあります。
このような問題を避けるためには、プログラマが共有メモリへのアクセスがいつ、どのように可視になるかを正確に理解している必要があります。このコミットは、Go言語のメモリモデルを公式に文書化することで、並行プログラムの正確性を保証するための基盤を提供することを目的としています。特に、異なるGoroutine間でのメモリ操作の順序付けと可視性に関するルールを明確にすることで、開発者が安全で予測可能な並行コードを書けるように支援します。
前提知識の解説
Happens Before (先行発生) 関係
「Happens Before」は、並行システムにおけるイベント間の順序関係を定義する概念です。イベント e1
がイベント e2
の「happens before」である場合、e1
の効果は e2
が開始する前に完全に完了し、e1
によって行われたすべての変更は e2
から可視であることが保証されます。もし2つのイベント間に「happens before」関係がない場合、それらは並行して発生すると見なされ、その相対的な順序は保証されません。
Go言語のメモリモデルでは、この「happens before」関係を用いて、共有変数への読み書きの可視性を保証します。
Goroutine
GoroutineはGo言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量であり、数千、数万のGoroutineを同時に実行することが可能です。GoroutineはGoランタイムによって管理され、マルチコアプロセッサ上で効率的にスケジューリングされます。
チャネル (Channels)
チャネルは、Goroutine間で値を送受信するための通信メカニズムであり、Goにおける主要な同期プリミティブです。チャネルを介した通信は、自動的に「happens before」関係を確立し、データ競合を避けるための安全な手段を提供します。
- バッファなしチャネル: 送信側は受信側が値を受け取るまでブロックし、受信側は送信側が値を送るまでブロックします。これにより、送信と受信が同期され、明確な「happens before」関係が確立されます。
- バッファありチャネル: バッファが満杯でない限り、送信側はブロックしません。バッファが空でない限り、受信側はブロックしません。バッファの存在により、バッファなしチャネルとは異なる「happens before」の保証が適用されます。
Mutex (sync.Mutex, sync.RWMutex)
Mutex(相互排他ロック)は、共有リソースへのアクセスを排他的に制御するための同期プリミティブです。sync.Mutex
は、一度に一つのGoroutineのみがロックを取得し、保護されたコードセクション(クリティカルセクション)を実行することを保証します。sync.RWMutex
は、読み取り操作は複数同時に許可するが、書き込み操作は排他的に行う、というより柔軟なロックを提供します。
sync.Once
sync.Once
は、特定の関数が一度だけ実行されることを保証するための同期プリミティブです。複数のGoroutineが同時に once.Do(f)
を呼び出しても、f
は一度だけ実行され、他の呼び出しは f
の実行が完了するまでブロックされます。これは、シングルトンの初期化や、一度だけ実行する必要があるセットアップ処理に非常に有用です。
誤った同期パターン
Go言語のメモリモデルを理解せずに並行処理を実装すると、以下のような一般的な落とし穴にはまる可能性があります。
- Double-Checked Locking (二重チェックロック): リソースの初期化コストを削減するために、ロックなしで初期化済みフラグをチェックし、未初期化の場合にのみロックを取得して再度チェックするパターンです。コンパイラやプロセッサによる命令の再配置により、フラグが設定されていてもリソースが完全に初期化されていない状態を他のGoroutineが観測してしまう可能性があり、Goでは安全ではありません。
sync.Once
を使用するのが正しいアプローチです。 - Busy Waiting (ビジーループ): ある条件が満たされるまで、ループ内で繰り返し条件をチェックし続けるパターンです。これはCPUリソースを無駄に消費し、システムのパフォーマンスを低下させます。Goでは、チャネルや
sync.Cond
を使用して、条件が満たされるまでGoroutineを効率的にブロックさせるべきです。
技術的詳細
Goメモリモデルは、主に以下のルールに基づいて「happens before」関係を定義し、共有メモリへのアクセスにおける可視性を保証します。
- 単一Goroutine内: 単一のGoroutine内では、読み書きはプログラムで指定された順序で実行されるかのように振る舞います。コンパイラやプロセッサは、そのGoroutine内の実行動作を変更しない限り、読み書きを再配置できます。
- 変数の初期化: 変数のゼロ値による初期化は、メモリモデルにおける書き込みとして扱われます。
- 複数マシンワードサイズの読み書き: 単一のマシンワードよりも大きい値の読み書きは、複数のマシンワードサイズの操作として、未指定の順序で振る舞います。これは、アトミックでない限り、複数ワードのデータ構造への読み書きが部分的にしか完了していない状態を他のGoroutineが観測する可能性があることを意味します。
同期イベントによる「happens before」の確立
Goメモリモデルは、特定の同期イベントが「happens before」関係を確立することを保証します。
- 初期化:
- パッケージ
p
がパッケージq
をインポートする場合、q
のinit
関数の完了は、p
のinit
関数の開始よりも先行して発生します。 main.main
関数の開始は、すべてのinit
関数が完了した後で発生します。init
関数中に作成されたGoroutineの実行は、すべてのinit
関数が完了した後で発生します。
- パッケージ
- Goroutineの作成: 新しいGoroutineを開始する
go
ステートメントは、そのGoroutineの実行が開始するよりも先行して発生します。 - チャネル通信:
- チャネルへの送信は、対応するチャネルからの受信が完了するよりも先行して発生します。
- バッファなしチャネルからの受信は、そのチャネルへの送信が完了するよりも先行して発生します。
- ロック (sync.Mutex):
sync.Mutex
変数l
について、n
番目のl.Unlock()
呼び出しは、m
番目のl.Lock()
呼び出しが戻るよりも先行して発生します(ただしn < m
)。 - Once (sync.Once):
once.Do(f)
の単一のf()
呼び出しは、once.Do(f)
が戻るよりも先行して発生します。
不適切な同期の例
ドキュメントでは、Goメモリモデルを理解せずに陥りやすい誤った同期パターンを具体例を挙げて説明しています。
- Double-Checked Lockingの誤用:
done
フラグの書き込みが観測されても、それに先行するa
への書き込みが観測される保証はないため、空文字列が出力される可能性があることを示しています。 - Busy Waitingの誤用:
done
フラグがtrue
になるまでループで待機する例です。done
への書き込みがmain
Goroutineによって観測される保証がないため、ループが終了しない可能性や、a
の値が初期化されていない状態で読み取られる可能性があることを指摘しています。 - ポインタの初期化とフィールドの初期化の分離: 構造体ポインタが
nil
でなくなったことを観測しても、そのポインタが指す構造体のフィールドが完全に初期化されている保証はないことを示しています。
これらの例はすべて、明示的な同期メカニズムを使用することの重要性を強調しています。
コアとなるコードの変更箇所
このコミットは、doc/mem.html
という新しいファイルをリポジトリに追加しています。このファイルは、Go言語のメモリモデルに関するHTML形式のドキュメントであり、545行の追加のみで構成されています。既存のファイルへの変更や削除はありません。
--- /dev/null
+++ b/doc/mem.html
@@ -0,0 +1,457 @@
+<h1>The Go memory model</h1>
+
+<h2>Introduction</h2>
+
+<p>
+The Go memory model specifies the conditions under which
+reads of a variable in one goroutine can be guaranteed to
+observe values produced by writes to the same variable in a different goroutine.
+</p>
+
+<h2>Happens Before</h2>
+
+<p>
+Within a single goroutine, reads and writes must behave
+as if they executed in the order specified by the program.
+That is, compilers and processors may reorder the reads and writes
+executed within a single goroutine only when the reordering
+does not change the execution behavior within that goroutine.
+Because of this reordering, the execution order observed
+by one may differ from the order perceived
+by another. For example, if one goroutine
+executes <code>a = 1; b = 2;</code>, a second goroutine might observe
+the updated value of <code>b</code> before the updated value of <code>a</code>.
+</p>
+
+... (以下、省略) ...
コアとなるコードの解説
追加された doc/mem.html
は、Go言語のメモリモデルを体系的に説明するドキュメントです。主要なセクションは以下の通りです。
-
Introduction (はじめに): Goメモリモデルの目的を簡潔に説明しています。異なるGoroutine間での変数への読み書きの可視性を保証する条件を定義することが目的です。
-
Happens Before (先行発生): 「happens before」という概念を導入し、これがGoプログラムにおけるメモリ操作の実行順序に対する部分的な順序付けであることを説明しています。単一Goroutine内での順序付け、および共有変数への読み書きが特定の書き込みを観測するための条件(許可される条件と保証される条件)を定義しています。
-
Synchronization (同期): Go言語が提供する主要な同期プリミティブが、どのように「happens before」関係を確立するかを具体的に説明しています。
- Initialization (初期化): パッケージの
init
関数やmain
関数の開始、Goroutineの作成に関する順序付けルール。 - Goroutine creation (Goroutineの作成):
go
ステートメントがGoroutineの実行開始よりも先行して発生すること。 - Channel communication (チャネル通信): 送信と受信がどのように「happens before」関係を確立するか。バッファなしチャネルとバッファありチャネルの違いも例を挙げて説明。
- Locks (ロック):
sync.Mutex
のLock()
とUnlock()
がどのように順序付けを保証するか。sync.RWMutex
についてはTODOとしています(コミット時点)。 - Once (sync.Once):
once.Do(f)
がf()
の一度だけの実行をどのように保証するか。
- Initialization (初期化): パッケージの
-
Incorrect synchronization (不適切な同期): Goメモリモデルを理解せずに陥りやすい一般的な並行処理の落とし穴を具体例を挙げて解説しています。
- Double-checked locking (二重チェックロック):
done
フラグの書き込みが観測されても、それに先行する他の書き込みが観測される保証がないため、誤った結果を招く可能性があること。 - Busy waiting (ビジーループ): 条件が満たされるまでループで待機するパターンが、CPUリソースの無駄遣いであるだけでなく、条件が観測されない可能性や、関連するデータが正しく初期化されていない可能性を指摘。
- ポインタの初期化とフィールドの初期化の分離: ポインタが
nil
でなくなったことを観測しても、そのポインタが指す構造体のフィールドが完全に初期化されている保証はないこと。 これらの例を通じて、明示的な同期の重要性を強調しています。
- Double-checked locking (二重チェックロック):
このドキュメントは、Go言語の並行処理のセマンティクスを理解し、データ競合を避けて堅牢な並行プログラムを構築するための基礎となる重要な情報を提供しています。
関連リンク
- Go言語公式ドキュメント: https://go.dev/doc/
- Go Concurrency Patterns: https://go.dev/blog/concurrency-patterns
参考にした情報源リンク
- The Go Memory Model - go.dev
- Go Memory Model - cmu.edu
- Understanding Go Memory Model - stackademic.com
- Go Concurrency Pitfalls: Double-Checked Locking and Busy Waiting - medium.com (具体的なURLは検索結果から推測)
- Go Synchronization Primitives: Channels, Mutexes, and sync.Once - geeksforgeeks.org (具体的なURLは検索結果から推測)
- Go Concurrency: Mutexes - go.dev
- Go Concurrency: sync.Once - dev.to (具体的なURLは検索結果から推測)
- Go Concurrency: Channels - yourbasic.org