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

[インデックス 15117] ファイルの概要

このコミットは、Go言語の標準ライブラリ net パッケージ内の lookup_windows.go ファイルに対する変更です。具体的には、Windows環境におけるネットワーク関連のルックアップ(プロトコル名、ホスト名、サービス名から情報を取得する)関数における競合状態(レースコンディション)を防止するための修正が施されています。

コミット

commit bc9999337b0c54a8035c4f9e6ea13b1fe3f34706
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Mon Feb 4 13:05:20 2013 +1100

    net: prevent races during windows lookup calls
    
    This only affects code (with exception of lookupProtocol)
    that is only executed on older versions of Windows.
    
    R=rsc, bradfitz
    CC=golang-dev
    https://golang.org/cl/7293043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/bc9999337b0c54a8035c4f9e6ea13b1fe3f34706

元コミット内容

このコミットは、Windows環境でのネットワークルックアップ関数(lookupProtocol, oldLookupIP, oldLookupPort)における競合状態を修正することを目的としています。以前のバージョンでは、これらの関数はsync.Mutexを使用して同期を取ろうとしていましたが、Windows APIの特定の挙動により、このアプローチでは不十分でした。

変更の背景

WindowsのネットワークルックアップAPI(例: GetProtoByName, GetHostByName, GetServByName)は、その戻り値をスレッドローカルストレージ(TLS: Thread Local Storage)に格納する場合があります。Goのランタイムは、複数のゴルーチンを少数のOSスレッドに多重化して実行します。このため、異なるゴルーチンが同時にこれらのWindows APIを呼び出し、それらが同じOSスレッド上で実行されると、一方のゴルーチンがAPIから返されたTLS内のデータを読み取る前に、別のゴルーチンがそのデータを上書きしてしまうという競合状態が発生する可能性がありました。

この問題は、特に古いバージョンのWindowsで顕著であり、netパッケージがこれらの古いAPIを使用している場合に発生します。sync.Mutexによるロックは、Goのコードレベルでの排他制御には有効ですが、OSスレッドレベルでのTLSデータの競合を防ぐことはできませんでした。

前提知識の解説

  1. ゴルーチンとOSスレッド:

    • ゴルーチン (Goroutine): Go言語の軽量な並行処理単位です。数千、数万のゴルーチンを同時に実行できます。Goランタイムは、これらのゴルーチンを少数のOSスレッドに効率的にマッピングして実行します。
    • OSスレッド (OS Thread): オペレーティングシステムが管理する実行単位です。CPUによって直接スケジュールされます。
    • M:Nスケジューリング: Goランタイムは、M個のゴルーチンをN個のOSスレッドに多重化するM:Nスケジューリングモデルを採用しています。これにより、ゴルーチンは特定のOSスレッドに固定されず、実行中に異なるOSスレッドに移動する可能性があります。
  2. スレッドローカルストレージ (TLS: Thread Local Storage):

    • 各OSスレッドが独立してアクセスできるメモリ領域です。グローバル変数とは異なり、TLSに格納されたデータは、そのスレッドからのみアクセス可能です。
    • 一部のC/C++ライブラリやOS APIは、内部状態や戻り値をTLSに格納することがあります。これは、関数がスレッドセーフであると同時に、呼び出し元にポインタを返す必要がない場合に便利です。
  3. 競合状態 (Race Condition):

    • 複数の並行プロセス(この場合はゴルーチン)が共有リソース(この場合はOSスレッドのTLS)にアクセスし、そのアクセス順序によって結果が非決定的に変わる状態を指します。
    • TLSを使用するAPIを複数のゴルーチンが呼び出す場合、Goランタイムがゴルーチンを同じOSスレッドにスケジュールすると、TLSデータが上書きされ、誤った結果が返される可能性があります。
  4. runtime.LockOSThread()runtime.UnlockOSThread():

    • runtime.LockOSThread(): 現在のゴルーチンを、現在実行されているOSスレッドに「ロック」します。このゴルーチンは、runtime.UnlockOSThread()が呼び出されるまで、そのOSスレッドから離れることはありません。
    • runtime.UnlockOSThread(): LockOSThreadによって確立されたロックを解除し、ゴルーチンが再び任意のOSスレッドにスケジュールされることを許可します。
    • これらの関数は、特定のOSスレッドに依存するC/C++ライブラリやOS APIをGoから安全に呼び出す必要がある場合に特に有用です。

技術的詳細

このコミットの技術的解決策は、Windows APIがTLSを使用するという特性に対応するため、GoのゴルーチンとOSスレッドの間の関係を一時的に制御することにあります。

  1. TLS問題の回避: WindowsのGetProtoByName, GetHostByName, GetServByNameといったAPIは、内部的にTLSを利用して結果を返します。GoのゴルーチンはOSスレッド間を自由に移動できるため、あるゴルーチンがこれらのAPIを呼び出してTLSに結果を書き込んだ直後に、別のゴルーチンが同じOSスレッドにスケジュールされ、別のAPI呼び出しによってTLSの内容を上書きしてしまう可能性があります。これにより、最初のゴルーチンが期待する結果を得られないという競合状態が発生します。

  2. runtime.LockOSThread() の活用: この問題を解決するため、各ルックアップ呼び出しは新しいゴルーチン内で実行され、そのゴルーチンはruntime.LockOSThread()を呼び出します。これにより、そのゴルーチンは特定のOSスレッドに固定され、API呼び出しが完了して結果が安全に取得されるまで、他のゴルーチンがそのOSスレッドのTLSを汚染するのを防ぎます。

  3. チャネルによる結果の伝達: runtime.LockOSThread()でOSスレッドにロックされたゴルーチンは、その結果をチャネルを通じて呼び出し元のゴルーチンに安全に返します。これにより、同期的なAPI呼び出しと同様のセマンティクスを維持しつつ、TLSの競合問題を回避しています。

  4. sync.Mutexの削除: 以前のコードではsync.Mutexを使用していましたが、これはGoのコードレベルでの排他制御には有効でも、OSスレッドのTLSに起因する競合状態には対処できませんでした。runtime.LockOSThread()によるOSスレッドの固定化というより低レベルな制御を導入することで、sync.Mutexは不要となり削除されました。

この変更は、Goのクロスプラットフォームな性質を維持しつつ、特定のOS(この場合はWindows)のAPIの挙動に起因する低レベルな競合状態を、Goランタイムの機能を使って効果的に解決した例と言えます。

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

変更は src/pkg/net/lookup_windows.go ファイルに集中しています。

  1. sync.Mutex の削除:

    -var (
    -	protoentLock sync.Mutex
    -	hostentLock  sync.Mutex
    -	serventLock  sync.Mutex
    -)
    

    syncパッケージのインポートも削除されています。

  2. runtime パッケージのインポート追加:

    -import (
    -	"os"
    -	"sync"
    -	"syscall"
    -	"unsafe"
    -)
    +import (
    +	"os"
    +	"runtime"
    +	"syscall"
    +	"unsafe"
    +)
    
  3. lookupProtocol の変更:

    • 元のlookupProtocolのロジックがgetprotobynameという新しい内部関数に移動。
    • lookupProtocolは、getprotobynameを新しいゴルーチン内で呼び出し、runtime.LockOSThread()runtime.UnlockOSThread()でOSスレッドを固定。
    • 結果はチャネルを通じて受け渡し。
    // Before:
    // func lookupProtocol(name string) (proto int, err error) {
    // 	protoentLock.Lock()
    // 	defer protoentLock.Unlock()
    // 	p, err := syscall.GetProtoByName(name)
    // 	// ...
    // }
    
    // After:
    func getprotobyname(name string) (proto int, err error) {
    	p, err := syscall.GetProtoByName(name)
    	if err != nil {
    		return 0, os.NewSyscallError("GetProtoByName", err)
    	}
    	return int(p.Proto), nil
    }
    
    func lookupProtocol(name string) (proto int, err error) {
    	// GetProtoByName return value is stored in thread local storage.
    	// Start new os thread before the call to prevent races.
    	type result struct {
    		proto int
    		err   error
    	}
    	ch := make(chan result)
    	go func() {
    		runtime.LockOSThread()
    		defer runtime.UnlockOSThread()
    		proto, err := getprotobyname(name)
    		ch <- result{proto: proto, err: err}
    	}()
    	r := <-ch
    	return r.proto, r.err
    }
    
  4. oldLookupIP の変更:

    • 元のoldLookupIPのロジックがgethostbynameという新しい内部関数に移動。
    • oldLookupIPは、gethostbynameを新しいゴルーチン内で呼び出し、runtime.LockOSThread()runtime.UnlockOSThread()でOSスレッドを固定。
    • 結果はチャネルを通じて受け渡し。
    // Before:
    // func oldLookupIP(name string) (addrs []IP, err error) {
    // 	hostentLock.Lock()
    // 	defer hostentLock.Unlock()
    // 	h, err := syscall.GetHostByName(name)
    // 	// ...
    // }
    
    // After:
    func gethostbyname(name string) (addrs []IP, err error) {
    	h, err := syscall.GetHostByName(name)
    	if err != nil {
    		return nil, os.NewSyscallError("GetHostByName", err)
    	}
    	// ...
    	return addrs, nil
    }
    
    func oldLookupIP(name string) (addrs []IP, err error) {
    	// GetHostByName return value is stored in thread local storage.
    	// Start new os thread before the call to prevent races.
    	type result struct {
    		addrs []IP
    		err   error
    	}
    	ch := make(chan result)
    	go func() {
    		runtime.LockOSThread()
    		defer runtime.UnlockOSThread()
    		addrs, err := gethostbyname(name)
    		ch <- result{addrs: addrs, err: err}
    	}()
    	r := <-ch
    	return r.addrs, r.err
    }
    
  5. oldLookupPort の変更:

    • 元のoldLookupPortのロジックがgetservbynameという新しい内部関数に移動。
    • oldLookupPortは、getservbynameを新しいゴルーチン内で呼び出し、runtime.LockOSThread()runtime.UnlockOSThread()でOSスレッドを固定。
    • 結果はチャネルを通じて受け渡し。
    // Before:
    // func oldLookupPort(network, service string) (port int, err error) {
    // 	// ...
    // 	serventLock.Lock()
    // 	defer serventLock.Unlock()
    // 	s, err := syscall.GetServByName(service, network)
    // 	// ...
    // }
    
    // After:
    func getservbyname(network, service string) (port int, err error) {
    	switch network {
    	case "tcp4", "tcp6":
    		network = "tcp"
    	case "udp4", "udp6":
    		network = "udp"
    	}
    	s, err := syscall.GetServByName(service, network)
    	if err != nil {
    		return 0, os.NewSyscallError("GetServByName", err)
    	}
    	return int(syscall.Ntohs(s.Port)), nil
    }
    
    func oldLookupPort(network, service string) (port int, err error) {
    	// GetServByName return value is stored in thread local storage.
    	// Start new os thread before the call to prevent races.
    	type result struct {
    		port int
    		err  error
    	}
    	ch := make(chan result)
    	go func() {
    		runtime.LockOSThread()
    		defer runtime.UnlockOSThread()
    		port, err := getservbyname(network, service)
    		ch <- result{port: port, err: err}
    	}()
    	r := <-ch
    	return r.port, r.err
    }
    

コアとなるコードの解説

このコミットの核心は、Windows APIのTLS(スレッドローカルストレージ)の挙動に起因する競合状態を、Goのruntime.LockOSThread()runtime.UnlockOSThread()を用いて回避する点にあります。

各ルックアップ関数(lookupProtocol, oldLookupIP, oldLookupPort)は、以下のパターンで再実装されています。

  1. 内部ヘルパー関数の導入: 元々ルックアップ関数内にあったsyscall呼び出しを含むロジックは、getprotobyname, gethostbyname, getservbynameといった新しい内部ヘルパー関数に切り出されました。これらのヘルパー関数は、純粋にWindows APIを呼び出し、その結果を返します。

  2. 新しいゴルーチンでの実行: 各ルックアップ関数は、ヘルパー関数の呼び出しを新しいゴルーチン(go func() { ... }())内で実行します。

  3. OSスレッドの固定: 新しいゴルーチンが開始されると、すぐにruntime.LockOSThread()が呼び出されます。これにより、そのゴルーチンは現在実行されているOSスレッドに「ロック」され、他のOSスレッドに移動したり、他のゴルーチンがそのOSスレッドにスケジュールされたりすることがなくなります。この固定化は、defer runtime.UnlockOSThread()によって、ゴルーチンが終了する際に解除されます。

    • このLockOSThreadの目的は、Windows APIがTLSに結果を書き込む際に、そのTLSが他のゴルーチンによって上書きされるのを防ぐことです。ゴルーチンが特定のOSスレッドに固定されることで、API呼び出しとその結果の読み取りが、そのスレッドのTLSに対して排他的に行われることが保証されます。
  4. チャネルによる結果の同期と伝達:

    • type result struct { ... }という構造体が定義され、ルックアップの結果(プロトコル番号、IPアドレスリスト、ポート番号)とエラーを保持します。
    • ch := make(chan result)というチャネルが作成され、新しいゴルーチンと呼び出し元のゴルーチン間の通信に使用されます。
    • 新しいゴルーチンは、ヘルパー関数の実行結果をch <- result{...}としてチャネルに送信します。
    • 呼び出し元のゴルーチンは、r := <-chとしてチャネルから結果を受信します。これにより、API呼び出しが完了するまで呼び出し元はブロックされ、結果が安全に伝達されます。

このメカニズムにより、GoのランタイムがゴルーチンをOSスレッドに多重化する際の柔軟性を維持しつつ、特定のOS APIのTLS依存性による競合状態を、必要な期間だけOSスレッドを固定するという形で解決しています。以前のsync.Mutexは、Goのコードレベルでの共有データ保護には有効でしたが、OSスレッドのTLSという低レベルな共有リソースの保護には不十分であったため、このより洗練されたアプローチに置き換えられました。

関連リンク

  • Go言語のIssueトラッカーやコードレビューシステム (Gerrit): https://golang.org/cl/7293043
  • Go言語のruntimeパッケージドキュメント: https://pkg.go.dev/runtime (特にLockOSThreadUnlockOSThreadについて)
  • Go言語のnetパッケージドキュメント: https://pkg.go.dev/net
  • Windows APIのドキュメント (例: GetProtoByName, GetHostByName, GetServByName): Microsoft Learn (MSDN)

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特にsrc/pkg/net/lookup_windows.goの履歴)
  • Go言語のIssueトラッカーおよびコードレビューコメント
  • スレッドローカルストレージ (TLS) に関する一般的な情報源
  • Windows APIに関するMicrosoftの公式ドキュメント