[インデックス 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.Once
の Do
メソッド内にラップしました。
var (
cgoLibGccFile string
cgoLibGccFileOnce sync.Once
)
cgoLibGccFileOnce
は sync.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
がエラーを返したか、空のファイル名を返したことを意味します)、追加のエラーチェックが行われます。- もし
err
がnil
であれば、"failed to get libgcc filename"
という新しいエラーが作成されます。これは、libgcc
の取得自体はエラーにならなかったものの、結果として有効なファイル名が得られなかった場合に備えるためです。 - 最終的に、
cgo
関数はnil, nil, err
を返して、エラーを呼び出し元に伝播させます。これにより、libgcc
ファイル名の取得に失敗した場合のビルドエラーがより明確になります。
- もし
これらの変更により、cgoLibGccFile
の初期化がスレッドセーフになり、データ競合が解消されました。また、libgcc
ファイル名の取得に関するエラーハンドリングも改善され、ビルドプロセスの堅牢性が向上しています。
関連リンク
- GitHub上のコミットページ: https://github.com/golang/go/commit/7171f533d0bc21c600ea070ed7d593f35f4a1d44
- Gerrit Code Review (Goプロジェクトのコードレビューシステム): https://golang.org/cl/6851099
参考にした情報源リンク
- コミット情報:
./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
に関する一般的な情報源