[インデックス 14418] ファイルの概要
このコミットは、Go言語の syscall パッケージにおける LazyDLL および LazyProc 型に存在するデータ競合(data race)を修正するものです。特にWindows環境でのDLL(Dynamic Link Library)およびプロシージャ(関数)の遅延ロードに関連する問題に対処しています。
コミット
commit 9b4aaa418fe415bae73a65f9be2dcbc642bb8edf
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Fri Nov 16 12:06:48 2012 +0400
syscall: fix data races in LazyDLL/LazyProc
Reincarnation of https://golang.org/cl/6817086 (sent from another account).
It is ugly because sync.Once will cause allocation of a closure.
Fixes #4343.
R=golang-dev, bradfitz, alex.brainman
CC=golang-dev
https://golang.org/cl/6856046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9b4aaa418fe415bae73a65f9be2dcbc642bb8edf
元コミット内容
このコミットは、syscall パッケージ内の LazyDLL および LazyProc 型におけるデータ競合を修正します。これは、以前に別のGoアカウントから送信された https://golang.org/cl/6817086 の再提出版です。コミットメッセージには、sync.Once を使用するとクロージャの割り当てが発生するため「醜い(ugly)」と述べられていますが、データ競合の修正を優先しています。この変更は、Issue #4343 を修正します。
変更の背景
Go言語の syscall パッケージは、オペレーティングシステム固有の低レベルな機能へのアクセスを提供します。Windows環境では、DLL(Dynamic Link Library)のロードや、DLL内のプロシージャ(関数)の取得がこれに該当します。LazyDLL と LazyProc は、これらのDLLやプロシージャを必要になったときに初めてロード(遅延ロード)するための構造体です。
遅延ロードは、アプリケーションの起動時間を短縮したり、必要のないリソースのロードを避けたりするのに役立ちます。しかし、複数のゴルーチン(goroutine)が同時に LazyDLL や LazyProc の Load() や Find() メソッドを呼び出す可能性がある場合、内部の状態(d.dll や p.proc)へのアクセスが同期されていないと、データ競合が発生する可能性があります。データ競合は、プログラムの予測不能な動作、クラッシュ、またはセキュリティ上の脆弱性につながる可能性があります。
このコミットは、Issue #4343 で報告されたデータ競合の問題を解決するために作成されました。元の変更セット https://golang.org/cl/6817086 が存在しましたが、何らかの理由で再提出が必要となり、このコミット https://golang.org/cl/6856046 として再提出されました。コミットメッセージにある「It is ugly because sync.Once will cause allocation of a closure.」という記述は、理想的な解決策ではないものの、データ競合の修正が喫緊の課題であったことを示唆しています。
前提知識の解説
- データ競合 (Data Race): 複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって順序付けされていない場合に発生するプログラミング上のバグです。Go言語では、データ競合は未定義の動作を引き起こす可能性があります。
- ゴルーチン (Goroutine): Go言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行できます。
syncパッケージ: Go言語の標準ライブラリで、並行処理における同期プリミティブ(ミューテックス、条件変数、Onceなど)を提供します。sync.Mutex: ミューテックス(相互排他ロック)を提供し、共有リソースへのアクセスを一度に1つのゴルーチンに制限することで、データ競合を防ぎます。sync/atomicパッケージ: 低レベルなアトミック操作(不可分操作)を提供します。これにより、ロックを使用せずに共有変数へのアクセスを安全に行うことができます。アトミック操作は、CPUの命令レベルで不可分性が保証されるため、データ競合を防ぎつつ、ミューテックスよりも高いパフォーマンスを発揮することがあります。unsafeパッケージ: Go言語の型安全性をバイパスする機能を提供します。unsafe.Pointerは、任意の型のポインタを保持できる特殊なポインタ型で、C言語のvoid*に似ています。これを使用することで、メモリを直接操作したり、異なる型のポインタ間で変換したりすることが可能になりますが、Goのメモリ安全性の保証が失われるため、非常に注意して使用する必要があります。- DLL (Dynamic Link Library): Windowsオペレーティングシステムで使用される共有ライブラリの形式です。複数のプログラムが同じDLLを共有することで、メモリ使用量を削減し、モジュール性を高めることができます。
syscallパッケージ: GoプログラムからOS固有のシステムコールを呼び出すためのパッケージです。Windowsでは、DLLのロードやプロシージャの呼び出しなどが含まれます。LazyDLL/LazyProc:syscallパッケージ内で定義されている構造体で、DLLやDLL内のプロシージャを必要になったときに初めてロード(遅延ロード)するためのものです。これにより、アプリケーションの起動時ではなく、実際にDLLやプロシージャが必要になった時点でリソースを確保できます。
技術的詳細
このコミットの主要な目的は、LazyDLL と LazyProc の Load() および Find() メソッドにおけるデータ競合を解消することです。
元のコードでは、d.dll == nil や p.proc == nil のような単純なポインタの比較と、その後の d.dll = dll や p.proc = proc のようなポインタへの直接代入が行われていました。これは、複数のゴルーチンが同時にこれらのメソッドを呼び出した場合に問題を引き起こします。
例えば、LazyDLL.Load() メソッドにおいて、以下のような競合状態が考えられます。
- ゴルーチンAが
d.dll == nilを評価し、trueとなる。 - ゴルーチンBが
d.dll == nilを評価し、trueとなる。 - ゴルーチンAがロックを取得し、DLLをロードし、
d.dll = dllを実行する。 - ゴルーチンBがロックを取得し、DLLをロードし、
d.dll = dllを実行する。
このシナリオでは、DLLが二重にロードされる可能性があります。さらに深刻なのは、d.dll や p.proc のポインタ値の読み書きがアトミックでない場合、部分的に更新されたポインタ値が他のゴルーチンから観測され、不正なメモリアクセスやクラッシュにつながる可能性があることです。
このコミットでは、sync/atomic パッケージの atomic.LoadPointer と atomic.StorePointer を使用することで、この問題を解決しています。
atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&d.dll))) == nil- これは
d.dll == nilの非競合バージョンです。d.dllのポインタ値をアトミックに読み込みます。 unsafe.Pointer(&d.dll)はd.dllのアドレスをunsafe.Pointer型に変換します。(*unsafe.Pointer)(...)は、そのunsafe.Pointerを*unsafe.Pointer型にキャストします。これはatomic.LoadPointerが期待する引数の型です。
- これは
atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&d.dll)), unsafe.Pointer(dll))- これは
d.dll = dllの非競合バージョンです。dllのポインタ値をアトミックにd.dllに書き込みます。 - 同様に、
unsafe.Pointer(&d.dll)とunsafe.Pointer(dll)を使用して、ポインタのアドレスと値をunsafe.Pointer型に変換しています。
- これは
これらのアトミック操作により、d.dll や p.proc のポインタ値の読み書きが不可分になり、複数のゴルーチンが同時にアクセスしてもデータ競合が発生しなくなります。
また、sync.Mutex も引き続き使用されています。これは、DLLやプロシージャの実際のロード処理(LoadLibrary や GetProcAddress の呼び出し)が一度だけ実行されることを保証するためです。アトミック操作はポインタの読み書きの安全性を保証しますが、初期化処理全体が一度だけ実行されることを保証するためには、依然としてミューテックスのようなより高レベルな同期プリミティブが必要です。
コミットメッセージにある「It is ugly because sync.Once will cause allocation of a closure.」というコメントは、sync.Once を使用すればより簡潔に初期化を一度だけ行うことができるが、その際にクロージャの割り当てが発生し、パフォーマンス上のオーバーヘッドがあることを示唆しています。しかし、このコミットでは、atomic 操作と sync.Mutex を組み合わせることで、データ競合を確実に防ぎつつ、パフォーマンスへの影響を最小限に抑えるアプローチを選択しています。
コアとなるコードの変更箇所
src/pkg/syscall/dll_windows.go ファイルの以下の部分が変更されています。
LazyDLL.Load() メソッド
--- a/src/pkg/syscall/dll_windows.go
+++ b/src/pkg/syscall/dll_windows.go
@@ -6,6 +6,8 @@ package syscall
import (
"sync"
+ "sync/atomic"
+ "unsafe"
)
// DLLError describes reasons for DLL load failures.
@@ -166,7 +168,9 @@ type LazyDLL struct {
// Load loads DLL file d.Name into memory. It returns an error if fails.
// Load will not try to load DLL, if it is already loaded into memory.
func (d *LazyDLL) Load() error {
- if d.dll == nil {
+ // Non-racy version of:
+ // if d.dll == nil {
+ if atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&d.dll))) == nil {
d.mu.Lock()
defer d.mu.Unlock()
if d.dll == nil {
@@ -174,7 +178,9 @@ func (d *LazyDLL) Load() error {
if e != nil {
return e
}
- d.dll = dll
+ // Non-racy version of:
+ // d.dll = dll
+ atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&d.dll)), unsafe.Pointer(dll))
}
}
return nil
LazyProc.Find() メソッド
--- a/src/pkg/syscall/dll_windows.go
+++ b/src/pkg/syscall/dll_windows.go
@@ -217,7 +223,9 @@ type LazyProc struct {
// an error if search fails. Find will not search procedure,
// if it is already found and loaded into memory.
func (p *LazyProc) Find() error {
- if p.proc == nil {
+ // Non-racy version of:
+ // if p.proc == nil {
+ if atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&p.proc))) == nil {
p.mu.Lock()
defer p.mu.Unlock()
if p.proc == nil {
@@ -229,7 +237,9 @@ func (p *LazyProc) Find() error {
if e != nil {
return e
}
- p.proc = proc
+ // Non-racy version of:
+ // p.proc = proc
+ atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&p.proc)), unsafe.Pointer(proc))
}
}
return nil
コアとなるコードの解説
変更の核心は、d.dll および p.proc フィールドへのアクセスをアトミック操作に置き換えた点です。
-
import文の追加:"sync/atomic"と"unsafe"パッケージが新しくインポートされています。atomicパッケージはアトミック操作のために、unsafeパッケージはポインタ型をunsafe.Pointerに変換するために必要です。 -
if d.dll == nilの変更: 元のif d.dll == nilは、if atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&d.dll))) == nilに変更されました。unsafe.Pointer(&d.dll):d.dllフィールドのアドレスを取得し、それをunsafe.Pointer型に変換します。これは、atomic.LoadPointerが*unsafe.Pointer型の引数を取るため、その準備です。(*unsafe.Pointer)(...):unsafe.Pointer型の値を*unsafe.Pointer型にキャストします。これにより、atomic.LoadPointerがポインタのポインタを受け取れるようになります。atomic.LoadPointer(...):d.dllが指すメモリ位置からポインタ値をアトミックに読み込みます。これにより、他のゴルーチンによる同時書き込みがあっても、常に完全で整合性のあるポインタ値が読み込まれることが保証されます。
-
d.dll = dllの変更: 元のd.dll = dllは、atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&d.dll)), unsafe.Pointer(dll))に変更されました。atomic.StorePointer(...):dllのポインタ値をd.dllが指すメモリ位置にアトミックに書き込みます。これにより、他のゴルーチンが同時にd.dllを読み込もうとしても、書き込みが完了するまで読み込みがブロックされるか、書き込みが完了した後の完全な値が読み込まれることが保証されます。
LazyProc.Find() メソッドについても、p.proc フィールドに対して全く同じロジックが適用されています。
これらの変更により、LazyDLL と LazyProc の内部状態(d.dll と p.proc)へのアクセスがデータ競合フリーになり、並行環境下での安定性と信頼性が向上しました。sync.Mutex は、DLLやプロシージャの実際のロード処理が一度だけ実行されることを保証するために引き続き使用されており、アトミック操作はポインタの読み書きの安全性を保証するという役割分担がなされています。
関連リンク
- Go Issue #4343: https://github.com/golang/go/issues/4343
- Go CL 6817086 (元の変更セット): https://golang.org/cl/6817086
- Go CL 6856046 (このコミットに対応する変更セット): https://golang.org/cl/6856046
参考にした情報源リンク
- Go言語の
sync/atomicパッケージに関する公式ドキュメント: https://pkg.go.dev/sync/atomic - Go言語の
unsafeパッケージに関する公式ドキュメント: https://pkg.go.dev/unsafe - Go言語におけるデータ競合の検出と修正に関する一般的な情報: https://go.dev/doc/articles/race_detector
- Go言語の
sync.Onceに関する公式ドキュメント: https://pkg.go.dev/sync#Once - Windows DLLの遅延ロードに関する一般的な情報 (Go言語に特化したものではないが、概念理解に役立つ): https://learn.microsoft.com/ja-jp/cpp/build/reference/delay-load-dlls?view=msvc-170
- Go言語の
syscallパッケージに関する公式ドキュメント: https://pkg.go.dev/syscall - Go言語のポインタと
unsafe.Pointerについての解説記事 (例): https://www.ardanlabs.com/blog/2019/07/pointers-in-go.html (これは一般的な情報源であり、特定のコミットに直接関連するものではありませんが、背景知識として有用です。) - Go言語の並行処理とデータ競合に関する一般的な情報源 (例): https://go.dev/blog/race-detector (これも一般的な情報源であり、特定のコミットに直接関連するものではありませんが、背景知識として有用です。)