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

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

このコミットは、Go言語のコマンドラインツール cmd/go におけるデータ競合(data race)の問題を修正することを目的としています。具体的には、cgoLibGccFile という変数へのアクセスに関するデータ競合を sync.Once を使用して解決し、libgcc ファイル名の取得処理を安全に初期化するように変更しています。

コミット

  • コミットハッシュ: 7171f533d0bc21c600ea070ed7d593f35f4a1d44
  • Author: Shenghou Ma minux.ma@gmail.com
  • Date: Fri Nov 23 19:58:46 2012 +0800
  • コミットメッセージ:
    cmd/go: fix data race on cgoLibGccFile
    Fixes #4426.
    
    R=dvyukov
    CC=golang-dev
    https://golang.org/cl/6851099
    

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

https://github.com/golang/go/commit/7171f533d0bc21c600ea070ed7d593f35f4a1d44

元コミット内容

cmd/go: fix data race on cgoLibGccFile
Fixes #4426.

R=dvyukov
CC=golang-dev
https://golang.org/cl/6851099

変更の背景

このコミットの主な背景は、Go言語のビルドツール cmd/go において、cgoLibGccFile というグローバル変数へのアクセス時に発生していたデータ競合の解消です。データ競合は、複数のゴルーチン(Goの軽量スレッド)が同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生する競合状態です。このような競合は予測不能な動作やプログラムのクラッシュを引き起こす可能性があります。

cgoLibGccFile は、Cgo(GoとC言語の相互運用機能)を使用する際に必要となる libgcc ライブラリのファイル名を格納するための変数です。このファイル名は、gcc -print-libgcc-file-name コマンドを実行して取得されます。この取得処理が複数回、異なるゴルーチンから同時に実行される可能性があり、その結果として cgoLibGccFile の初期化が非同期に行われ、データ競合が発生していました。

コミットメッセージにある Fixes #4426 は、Goプロジェクトの内部的な課題追跡システムにおける問題番号を示しています。GitHubの公開リポジトリでは直接この番号の問題が見つからない場合もありますが、これはGo開発チームが内部で管理しているバグや改善点に対応するものです。この問題は、cgo を利用するGoプログラムのビルド時に、特定の条件下で不安定性や誤動作を引き起こす可能性があったため、修正が必要とされました。

前提知識の解説

1. データ競合 (Data Race)

データ競合は、並行プログラミングにおける一般的なバグの一種です。以下の3つの条件がすべて満たされたときに発生します。

  • 少なくとも2つのゴルーチン(またはスレッド)が同時に同じメモリ位置にアクセスする。
  • 少なくとも1つのアクセスが書き込み操作である。
  • アクセスが同期メカニズムによって保護されていない。

データ競合が発生すると、プログラムの実行結果がアクセス順序に依存するようになり、予測不能な結果(例: 間違った値の読み込み、プログラムのクラッシュ)を引き起こす可能性があります。Go言語では、go run -race コマンドでデータ競合検出器を有効にして実行することで、データ競合を検出できます。

2. sync.Once

sync.Once はGo言語の標準ライブラリ sync パッケージで提供される同期プリミティブです。その名の通り、特定の処理を「一度だけ」実行することを保証します。複数のゴルーチンから Do メソッドが呼び出された場合でも、引数として渡された関数は一度だけ実行され、その後の呼び出しでは何も行われません。これは、リソースの初期化や設定など、一度だけ実行すればよい処理を並行環境で安全に行うために非常に有用です。

sync.Once の内部では、ミューテックス(相互排他ロック)とアトミック操作を組み合わせて、スレッドセーフな一度きりの実行を保証しています。

3. Cgo

Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。これにより、既存のCライブラリをGoプロジェクトで再利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。Cgoを使用すると、Goのビルドプロセス中にCコンパイラ(通常はGCC)が呼び出され、Cコードがコンパイル・リンクされます。

4. libgcc

libgcc は、GCC(GNU Compiler Collection)が提供するランタイムサポートライブラリです。これは、GCCが生成するコードが依存する低レベルのヘルパー関数(例: 整数除算、浮動小数点演算、例外処理など)を提供します。Cgoを使用してCコードをGoプログラムにリンクする場合、GCCが使用されるため、libgcc が必要になることがあります。gcc -print-libgcc-file-name コマンドは、システム上で libgcc ライブラリがどこに存在するかを特定するために使用されます。

5. go build プロセス

go build コマンドは、Goのソースコードをコンパイルして実行可能ファイルを生成するプロセスです。このプロセスには、依存関係の解決、ソースファイルのコンパイル、リンクなどが含まれます。Cgoを使用する場合、go build はCコンパイラを呼び出し、CコードのコンパイルとGoコードとのリンクも管理します。

技術的詳細

このコミットは、cgoLibGccFile というグローバル変数の初期化におけるデータ競合を解決するために、Goの sync.Once を導入しています。

変更前は、cgo 関数内で cgoLibGccFile が空文字列である場合に、b.libgcc(p) を呼び出して libgcc のファイル名を取得し、その結果を cgoLibGccFile に代入していました。このチェックと代入のロジックは、複数のゴルーチンが同時に cgo 関数を呼び出す可能性があるため、データ競合の温床となっていました。具体的には、あるゴルーチンが cgoLibGccFile == "" を評価して true となった直後に、別のゴルーチンが cgoLibGccFile に値を書き込むと、最初のゴルーチンが古い(または不完全な)値を読み込んだり、二重に初期化を試みたりする可能性があります。

この問題を解決するために、cgoLibGccFile の初期化を sync.OnceDo メソッド内にラップしました。

var (
	cgoLibGccFile     string
	cgoLibGccFileOnce sync.Once
)

cgoLibGccFileOncesync.Once 型の変数で、cgoLibGccFile の初期化を一度だけ実行することを保証します。

cgoLibGccFileOnce.Do(func() {
	cgoLibGccFile, err = b.libgcc(p)
})

このコードブロックにより、b.libgcc(p) の呼び出しと cgoLibGccFile への代入は、プログラムの実行中に一度だけ、かつスレッドセーフに実行されることが保証されます。Do メソッドに渡された無名関数は、cgoLibGccFileOnce が初めて Do を呼び出されたときにのみ実行されます。それ以降の Do の呼び出しでは、この関数は実行されません。

また、b.libgcc(p) のエラーハンドリングも改善されています。変更前は b.libgcc がエラーを返しても、cgo 関数は空文字列を返していました。変更後は、gcc -print-libgcc-file-name コマンドの実行に失敗した場合、より具体的なエラーメッセージを返すように修正されています。さらに、cgoLibGccFile が空のままである場合(例えば、libgcc の取得に失敗した場合)、"failed to get libgcc filename" というエラーを返すように変更され、エラーの伝播がより明確になりました。

この修正により、cmd/go のビルドプロセスにおける並行処理の安全性が向上し、特にCgoを使用する際の安定性が確保されます。

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

src/cmd/go/build.go ファイルにおける変更点は以下の通りです。

--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -1478,7 +1478,7 @@ func gccgoCleanPkgpath(p *Package) string {
 func (b *builder) libgcc(p *Package) (string, error) {
 	f, err := b.runOut(p.Dir, p.ImportPath, b.gccCmd(p.Dir), "-print-libgcc-file-name")
 	if err != nil {
-		return "", nil
+		return "", fmt.Errorf("gcc -print-libgcc-file-name: %v (%s)", err, f)
 	}
 	return strings.Trim(string(f), "\r\n"), nil
 }
@@ -1542,7 +1542,10 @@ func envList(key string) []string {
 
 var cgoRe = regexp.MustCompile(`[/\\:]`)
 
-var cgoLibGccFile string
+var (
+	cgoLibGccFile     string
+	cgoLibGccFileOnce sync.Once
+)
 
 func (b *builder) cgo(p *Package, cgoExe, obj string, gccfiles []string) (outGo, outObj []string, err error) {
 	if goos != toolGOOS {
@@ -1633,13 +1636,17 @@ func (b *builder) cgo(p *Package, cgoExe, obj string, gccfiles []string) (outGo,
 		bareLDFLAGS = append(bareLDFLAGS, f)
 		}
 	}
-	if cgoLibGccFile == "" {
-		var err error
+
+	cgoLibGccFileOnce.Do(func() {
 		cgoLibGccFile, err = b.libgcc(p)
-		if err != nil {
-			return nil, nil, err
+	})
+	if cgoLibGccFile == "" {
+		if err == nil {
+			err = errors.New("failed to get libgcc filename")
 		}
+		return nil, nil, err
 	}
+
 	var staticLibs []string
 	if goos == "windows" {
 		// libmingw32 and libmingwex might also use libgcc, so libgcc must come last

コアとなるコードの解説

func (b *builder) libgcc(p *Package) (string, error) の変更

  • 変更前: if err != nil { return "", nil }
    • gcc -print-libgcc-file-name コマンドの実行に失敗した場合、エラーを無視して空文字列を返していました。これは、エラーが発生したことを呼び出し元に適切に伝えられない問題がありました。
  • 変更後: if err != nil { return "", fmt.Errorf("gcc -print-libgcc-file-name: %v (%s)", err, f) }
    • エラーが発生した場合、fmt.Errorf を使用して、元のエラーとコマンドの出力 f を含む、より詳細なエラーメッセージを返すように変更されました。これにより、問題の診断が容易になります。

グローバル変数の宣言

  • 変更前: var cgoLibGccFile string
    • cgoLibGccFile は単なるグローバル変数として宣言されていました。
  • 変更後:
    var (
    	cgoLibGccFile     string
    	cgoLibGccFileOnce sync.Once
    )
    
    • cgoLibGccFile に加えて、sync.Once 型の cgoLibGccFileOnce が追加されました。これは、cgoLibGccFile の初期化を一度だけ実行するための同期プリミティブです。

func (b *builder) cgo(...) 内の cgoLibGccFile 初期化ロジックの変更

  • 変更前:
    if cgoLibGccFile == "" {
    	var err error
    	cgoLibGccFile, err = b.libgcc(p)
    	if err != nil {
    		return nil, nil, err
    	}
    }
    
    • cgoLibGccFile が空の場合に b.libgcc(p) を呼び出して初期化していました。この if 文は複数のゴルーチンから同時に評価される可能性があり、データ競合を引き起こす可能性がありました。また、b.libgcc から返されたエラーはここで処理されていましたが、cgoLibGccFile が空のままであることとエラーの関連性が不明瞭でした。
  • 変更後:
    cgoLibGccFileOnce.Do(func() {
    	cgoLibGccFile, err = b.libgcc(p)
    })
    if cgoLibGccFile == "" {
    	if err == nil {
    		err = errors.New("failed to get libgcc filename")
    	}
    	return nil, nil, err
    }
    
    • cgoLibGccFileOnce.Do(func() { ... }) を使用することで、cgoLibGccFile の初期化処理(b.libgcc(p) の呼び出しと結果の代入)が、プログラムの実行中に一度だけ、かつスレッドセーフに実行されることが保証されます。
    • cgoLibGccFile が初期化後も空のままである場合(これは b.libgcc がエラーを返したか、空のファイル名を返したことを意味します)、追加のエラーチェックが行われます。
      • もし errnil であれば、"failed to get libgcc filename" という新しいエラーが作成されます。これは、libgcc の取得自体はエラーにならなかったものの、結果として有効なファイル名が得られなかった場合に備えるためです。
      • 最終的に、cgo 関数は nil, nil, err を返して、エラーを呼び出し元に伝播させます。これにより、libgcc ファイル名の取得に失敗した場合のビルドエラーがより明確になります。

これらの変更により、cgoLibGccFile の初期化がスレッドセーフになり、データ競合が解消されました。また、libgcc ファイル名の取得に関するエラーハンドリングも改善され、ビルドプロセスの堅牢性が向上しています。

関連リンク

参考にした情報源リンク

  • コミット情報: ./commit_data/14458.txt
  • Go言語の sync パッケージに関する公式ドキュメント: https://pkg.go.dev/sync (特に sync.Once のセクション)
  • Go言語のCgoに関する公式ドキュメント: https://go.dev/blog/c-go-cgo
  • データ競合に関する一般的な情報源 (例: Go言語のデータ競合検出器に関するドキュメントなど)
  • GCCの libgcc に関する一般的な情報源