[インデックス 10882] ファイルの概要
このコミットは、Go言語の標準ライブラリである archive/zip
パッケージのテストコード (reader_test.go
) におけるデータ競合(data race)を修正するものです。具体的には、テスト内で複数のGoroutineが並行して実行される際に、ループ変数のキャプチャが原因で発生していた競合状態を解消しています。
コミット
commit 315b361f898e2a7f299a742be5cfcb56c04d5c9d
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Dec 19 15:40:10 2011 -0800
zip: fix data race in test
R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/5492073
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/315b361f898e2a7f299a742be5cfcb56c04d5c9d
元コミット内容
zip: fix data race in test
R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/5492073
変更の背景
Go言語のような並行処理をサポートする言語では、複数のGoroutine(軽量スレッド)が同時に同じメモリ領域にアクセスし、少なくとも一方が書き込みを行う場合に「データ競合」が発生する可能性があります。データ競合は、プログラムの実行結果が非決定論的になり、デバッグが困難なバグ(例:クラッシュ、不正な値、デッドロック)を引き起こす原因となります。
このコミットの背景には、archive/zip
パッケージのテストコード reader_test.go
内で、並行してファイル読み込みテストを行う際にデータ競合が発生していたという問題があります。テストはプログラムの正しさを保証するためのものですが、テスト自体がデータ競合によって不安定になったり、誤った結果を出したりすると、その信頼性が損なわれます。このデータ競合は、テストがループ内でGoroutineを起動し、ループ変数をクロージャがキャプチャするGo言語でよく見られるパターンに起因していました。
テストの信頼性を確保し、将来的なバグの混入を防ぐために、このデータ競合を修正する必要がありました。
前提知識の解説
Go言語の並行処理とGoroutine
Go言語は、並行処理を言語レベルで強力にサポートしています。その中心となるのが「Goroutine」と「チャネル(channel)」です。
- Goroutine:
go
キーワードを使って関数呼び出しの前に記述することで、その関数を新しいGoroutineとして並行に実行します。GoroutineはOSのスレッドよりもはるかに軽量であり、数千、数万といった単位で簡単に生成できます。 - チャネル: Goroutine間の安全な通信と同期のための仕組みです。チャネルを通じて値を送受信することで、共有メモリを直接操作することなく、Goroutine間でデータをやり取りできます。
データ競合 (Data Race)
データ競合は、以下の3つの条件がすべて満たされたときに発生します。
- 少なくとも2つのGoroutineが同時に同じメモリ領域にアクセスする。
- それらのアクセスが少なくとも1つは書き込み操作である。
- それらのアクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。
データ競合が発生すると、プログラムの動作が予測不能になり、デバッグが非常に困難になります。Go言語には、データ競合を検出するための「Race Detector」というツールが組み込まれており、go run -race
や go test -race
のように -race
フラグを付けて実行することで、実行時にデータ競合を検出できます。
クロージャとループ変数
Go言語のクロージャ(匿名関数)は、それが定義された環境(外部スコープ)の変数を「キャプチャ」します。これは非常に強力な機能ですが、ループ内でGoroutineを起動する際には注意が必要です。
一般的な落とし穴は以下の通りです。
for i := 0; i < 5; i++ {
go func() {
// ここで i を使用すると、Goroutineが実行される頃には
// i の値はループの最終値(この場合は 5)になっている可能性がある
fmt.Println(i)
}()
}
このコードでは、i
はループ全体で共有される単一の変数です。各Goroutineが起動されるとき、i
の値はまだ確定していません。Goroutineが実際に実行されるのは、ループが終了した後になることが多く、その時点で i
は最終的な値(例: 5)になっています。結果として、すべてのGoroutineが 5
を出力する可能性があります。
この問題を解決するには、ループ変数の値をGoroutineに「コピー」して渡す必要があります。
for i := 0; i < 5; i++ {
// i の値を引数としてGoroutineに渡す
go func(val int) {
fmt.Println(val) // val は各Goroutineで異なる値を持つ
}(i) // ここで i の現在の値が val にコピーされる
}
このようにすることで、各Goroutineはループ変数のその時点での値のコピーを受け取り、データ競合や意図しない動作を防ぐことができます。
archive/zip
パッケージ
archive/zip
はGo言語の標準ライブラリの一部で、ZIPアーカイブの読み書きをサポートします。このパッケージは、ZIPファイルの構造を解析し、個々のファイル(エントリ)にアクセスするための機能を提供します。
技術的詳細
このコミットで修正されたデータ競合は、src/pkg/archive/zip/reader_test.go
内の readTestZip
関数で発生していました。この関数は、テスト用のZIPファイルから複数のファイルを並行して読み込むテストを行っていました。
問題のコードは以下の部分です。
for i := 0; i < 5; i++ {
for j, ft := range zt.File {
go func() { // 問題の箇所
readTestFile(t, ft, z.File[j])
done <- true
}()
n++
}
}
ここで、内側の for j, ft := range zt.File
ループ内でGoroutineが起動されています。このGoroutineの匿名関数は、ループ変数である j
と ft
をクロージャとしてキャプチャしています。
前述の「クロージャとループ変数」の解説の通り、j
と ft
はループの各イテレーションで新しい値に更新されますが、Goroutineが実際に実行される時点では、これらの変数はループの最終的な値を持っている可能性があります。
具体的には、複数のGoroutineが j
と ft
という同じメモリ位置を参照しようとするため、readTestFile
関数内で z.File[j]
にアクセスする際に、j
の値がGoroutine間で競合し、意図しないファイルが読み込まれたり、配列の範囲外アクセスが発生したりする可能性がありました。ft
も同様に、Goroutineが実行される前にループの次のイテレーションで上書きされる可能性がありました。
この問題を解決するために、コミットでは j
と ft
の値をGoroutineの匿名関数の引数として明示的に渡すように変更しました。これにより、各Goroutineは起動された時点での j
と ft
の値のコピーを受け取り、他のGoroutineやループの進行によって値が変更される影響を受けなくなります。
コアとなるコードの変更箇所
--- a/src/pkg/archive/zip/reader_test.go
+++ b/src/pkg/archive/zip/reader_test.go
@@ -163,10 +163,10 @@ func readTestZip(t *testing.T, zt ZipTest) {
done := make(chan bool)
for i := 0; i < 5; i++ {
for j, ft := range zt.File {
- go func() {
+ go func(j int, ft ZipTestFile) {
readTestFile(t, ft, z.File[j])
done <- true
- }()
+ }(j, ft)
n++
}
}
コアとなるコードの解説
変更点は以下の1行です。
- go func() {
+ go func(j int, ft ZipTestFile) {
そして、Goroutineの起動部分も変更されています。
- }()
+ }(j, ft)
この変更により、Goroutineの匿名関数が j
と ft
を引数として受け取るように定義され、その直後に (j, ft)
と記述することで、ループの現在のイテレーションにおける j
と ft
の値が、それぞれ j
と ft
という名前のGoroutineローカルな変数にコピーされて渡されます。
go func(j int, ft ZipTestFile)
: これは、j
とft
という2つの引数を受け取る匿名関数を定義しています。これらの引数は、Goroutineのスコープ内で独立した変数として機能します。(j, ft)
: これは、定義された匿名関数を呼び出す部分です。ここで、外側のループのj
とft
の現在の値が、匿名関数の引数j
とft
にそれぞれ「値渡し」されます。
この修正により、各Goroutineは自身の j
と ft
のコピーを持つため、他のGoroutineやループの次のイテレーションによってこれらの変数が変更されても影響を受けません。これにより、テストにおけるデータ競合が解消され、テストの信頼性が向上しました。
関連リンク
- Gerrit Change-ID: https://golang.org/cl/5492073
参考にした情報源リンク
- Go言語の公式ドキュメント(並行処理、クロージャに関するセクション)
- Go言語におけるデータ競合に関する一般的な知識とベストプラクティス
- Go言語のRace Detectorに関する情報