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

[インデックス 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内のプロシージャ(関数)の取得がこれに該当します。LazyDLLLazyProc は、これらのDLLやプロシージャを必要になったときに初めてロード(遅延ロード)するための構造体です。

遅延ロードは、アプリケーションの起動時間を短縮したり、必要のないリソースのロードを避けたりするのに役立ちます。しかし、複数のゴルーチン(goroutine)が同時に LazyDLLLazyProcLoad()Find() メソッドを呼び出す可能性がある場合、内部の状態(d.dllp.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やプロシージャが必要になった時点でリソースを確保できます。

技術的詳細

このコミットの主要な目的は、LazyDLLLazyProcLoad() および Find() メソッドにおけるデータ競合を解消することです。

元のコードでは、d.dll == nilp.proc == nil のような単純なポインタの比較と、その後の d.dll = dllp.proc = proc のようなポインタへの直接代入が行われていました。これは、複数のゴルーチンが同時にこれらのメソッドを呼び出した場合に問題を引き起こします。

例えば、LazyDLL.Load() メソッドにおいて、以下のような競合状態が考えられます。

  1. ゴルーチンAが d.dll == nil を評価し、true となる。
  2. ゴルーチンBが d.dll == nil を評価し、true となる。
  3. ゴルーチンAがロックを取得し、DLLをロードし、d.dll = dll を実行する。
  4. ゴルーチンBがロックを取得し、DLLをロードし、d.dll = dll を実行する。

このシナリオでは、DLLが二重にロードされる可能性があります。さらに深刻なのは、d.dllp.proc のポインタ値の読み書きがアトミックでない場合、部分的に更新されたポインタ値が他のゴルーチンから観測され、不正なメモリアクセスやクラッシュにつながる可能性があることです。

このコミットでは、sync/atomic パッケージの atomic.LoadPointeratomic.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.dllp.proc のポインタ値の読み書きが不可分になり、複数のゴルーチンが同時にアクセスしてもデータ競合が発生しなくなります。

また、sync.Mutex も引き続き使用されています。これは、DLLやプロシージャの実際のロード処理(LoadLibraryGetProcAddress の呼び出し)が一度だけ実行されることを保証するためです。アトミック操作はポインタの読み書きの安全性を保証しますが、初期化処理全体が一度だけ実行されることを保証するためには、依然としてミューテックスのようなより高レベルな同期プリミティブが必要です。

コミットメッセージにある「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 フィールドへのアクセスをアトミック操作に置き換えた点です。

  1. import 文の追加: "sync/atomic""unsafe" パッケージが新しくインポートされています。atomic パッケージはアトミック操作のために、unsafe パッケージはポインタ型を unsafe.Pointer に変換するために必要です。

  2. 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 が指すメモリ位置からポインタ値をアトミックに読み込みます。これにより、他のゴルーチンによる同時書き込みがあっても、常に完全で整合性のあるポインタ値が読み込まれることが保証されます。
  3. 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 フィールドに対して全く同じロジックが適用されています。

これらの変更により、LazyDLLLazyProc の内部状態(d.dllp.proc)へのアクセスがデータ競合フリーになり、並行環境下での安定性と信頼性が向上しました。sync.Mutex は、DLLやプロシージャの実際のロード処理が一度だけ実行されることを保証するために引き続き使用されており、アトミック操作はポインタの読み書きの安全性を保証するという役割分担がなされています。

関連リンク

参考にした情報源リンク