[インデックス 14690] ファイルの概要
このコミットは、Go言語の標準ライブラリの一部である go/token
パッケージ内の position_test.go
ファイルに対する変更です。go/token
パッケージは、Goソースコードの字句解析(lexing)や構文解析(parsing)において、ファイル名、行番号、列番号といったソースコード上の位置情報を管理するための重要な機能を提供します。position_test.go
は、このパッケージの機能、特に FileSet
や Pos
といった位置情報管理に関連する機能の正確性と堅牢性を検証するためのテストコードを含んでいます。
コミット
このコミットは、go/token
パッケージの FileSet.Pos
メソッドの並行利用に関するテストを追加するものです。具体的には、FileSet
の内部的な位置キャッシュにおける競合状態(race condition)を露呈させるためのテストケースが導入されました。これにより、複数のゴルーチンが同時に FileSet.Pos
を呼び出した際に発生しうる潜在的な問題を特定し、将来的な修正の検証を可能にすることを目的としています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4e406a2372b65ce58b2c4d26ada1c8b27c791af8
元コミット内容
commit 4e406a2372b65ce58b2c4d26ada1c8b27c791af8
Author: Dave Cheney <dave@cheney.net>
Date: Tue Dec 18 16:38:00 2012 -0800
go/token: add test for concurrent use of FileSet.Pos
Update #4354.
Add a test to expose the race in the FileSet position cache.
R=dvyukov, gri
CC=fullung, golang-dev
https://golang.org/cl/6940078
変更の背景
この変更の背景には、Go言語のツールチェイン(コンパイラ、リンター、フォーマッターなど)がソースコードの位置情報を扱う際に使用する go/token
パッケージの FileSet
型に潜在的な競合状態が存在するという問題がありました。具体的には、FileSet.Pos
メソッドが内部的に使用する位置キャッシュが、複数のゴルーチンから同時にアクセスされた場合に安全ではない可能性が指摘されていました(Issue #4354)。
このような競合状態は、プログラムの予測不能な動作、データの破損、あるいはクラッシュを引き起こす可能性があります。特に、Go言語のツールは並行処理を多用するため、このような低レベルのパッケージにおける競合状態は深刻な問題となりえます。このコミットは、この問題を修正する前に、まずその存在を確実に検出できるテストケースを導入することを目的としています。テストが失敗することで問題が露呈し、その後の修正が正しく行われたことを検証できるようになります。
前提知識の解説
Go言語の go/token
パッケージ
go/token
パッケージは、Go言語のソースコードを解析する際に、コード内の特定の位置(ファイル名、行番号、列番号)を効率的に表現・管理するための型と関数を提供します。これは、コンパイラ、リンター、デバッガーなどのツールが、エラーメッセージの表示やコードのナビゲーションを行う上で不可欠な情報源となります。
FileSet
: 複数のソースファイルをまとめて管理するコレクションです。各ファイルはFile
オブジェクトとしてFileSet
に追加され、それぞれが独自のオフセット範囲を持ちます。FileSet
は、プログラム全体のオフセット(Pos
型)から、特定のファイル内の行番号や列番号といった詳細な位置情報(Position
型)への変換を可能にします。Pos
: ソースコード全体におけるバイトオフセットを表す型です。これは、特定の文字がファイルの先頭から何バイト目に位置するかを示します。Pos
は、FileSet
を介して具体的なPosition
情報に変換されます。Position
: ソースコード内の具体的な位置(ファイル名、行番号、列番号)を表す構造体です。
レースコンディション (Race Condition)
レースコンディションとは、複数の並行に実行されるプロセスやスレッド(Go言語ではゴルーチン)が、共有リソース(メモリ上の変数、データ構造など)に同時にアクセスし、そのアクセス順序によってプログラムの最終的な結果が非決定的に変化してしまう状態を指します。これは、特に読み書きが混在するアクセスにおいて発生しやすく、デバッグが困難なバグの原因となります。
Go言語の sync
パッケージ
sync
パッケージは、Go言語で並行処理を安全に行うための基本的な同期プリミティブを提供します。
sync.WaitGroup
: 複数のゴルーチンの完了を待つためのメカニズムです。カウンタを持ち、Add
でカウンタを増やし、Done
で減らします。Wait
を呼び出すと、カウンタがゼロになるまでブロックします。これにより、メインゴルーチンが全てのバックグラウンドゴルーチンの処理完了を待つことができます。
Go言語の math/rand
パッケージ
math/rand
パッケージは、擬似乱数を生成するための機能を提供します。このテストでは、FileSet.Pos
に問い合わせる Pos
値をランダムに生成するために使用されています。
技術的詳細
このコミットで追加されたテスト TestFileSetRace
は、FileSet.Pos
メソッドが内部的に持つ位置キャッシュの競合状態を意図的に引き起こすように設計されています。
FileSet
の準備: まず、NewFileSet()
で新しいFileSet
インスタンスを作成します。- 多数のダミーファイルの追加:
for i := 0; i < 100; i++ { fset.AddFile(fmt.Sprintf("file-%d", i), fset.Base(), 1031) }
のループにより、100個のダミーファイルがFileSet
に追加されます。これにより、FileSet
の内部キャッシュが多くの位置情報で初期化され、競合状態が発生しやすい環境が作られます。fset.Base()
は、新しいファイルの開始オフセットを決定するために使用され、各ファイルが異なるオフセット範囲を持つようにします。 - 最大オフセットの取得:
max := int32(fset.Base())
で、FileSet
が管理するオフセットの最大値を取得します。これは、後でランダムなPos
値を生成する際の範囲として使用されます。 - 並行処理のセットアップ:
var stop sync.WaitGroup
でWaitGroup
を初期化します。for i := 0; i < 2; i++
のループで2つのゴルーチンを起動します。- 各ゴルーチンは
stop.Add(1)
でWaitGroup
のカウンタを増やします。 go func() { ... }()
で匿名関数をゴルーチンとして実行します。
- 競合の誘発:
- 各ゴルーチン内で
r := rand.New(rand.NewSource(r.Int63()))
を使用して、独立した乱数ジェネレータを作成します。これにより、各ゴルーチンが異なるシーケンスのランダムなPos
値を生成し、FileSet
のキャッシュの異なる部分にアクセスするようにします。 for i := 0; i < 1000; i++ { fset.Position(Pos(r.Int31n(max))) }
のループにより、各ゴルーチンは1000回、ランダムに生成されたPos
値を使ってfset.Position
メソッドを呼び出します。これにより、FileSet
の内部キャッシュに対して大量の並行読み取りアクセスが発生し、競合状態が露呈する可能性が高まります。
- 各ゴルーチン内で
- ゴルーチンの完了待機: 各ゴルーチンは処理が完了すると
stop.Done()
を呼び出し、WaitGroup
のカウンタを減らします。メインゴルーチンはstop.Wait()
を呼び出すことで、全てのゴルーチンが完了するまで待機します。
このテストは、go run -race
フラグを付けて実行された際に、FileSet.Pos
の内部キャッシュに競合状態が存在すれば、Goのレース検出器によって警告が報告されることを期待しています。テスト自体がパニックを起こしたり、不正な結果を返したりすることなく完了すれば、競合状態が修正されたことを示唆します。
コアとなるコードの変更箇所
diff --git a/src/pkg/go/token/position_test.go b/src/pkg/go/token/position_test.go
index 160107df40..3e7d552b75 100644
--- a/src/pkg/go/token/position_test.go
+++ b/src/pkg/go/token/position_test.go
@@ -6,6 +6,8 @@ package token
import (
"fmt"
+ "math/rand"
+ "sync"
"testing"
)
@@ -179,3 +181,26 @@ func TestFiles(t *testing.T) {
}
}\n }\n+\n+// issue 4345. Test concurrent use of FileSet.Pos does not trigger a
+// race in the FileSet position cache.
+func TestFileSetRace(t *testing.T) {
+ fset := NewFileSet()
+ for i := 0; i < 100; i++ {
+ fset.AddFile(fmt.Sprintf("file-%d", i), fset.Base(), 1031)
+ }
+ max := int32(fset.Base())
+ var stop sync.WaitGroup
+ r := rand.New(rand.NewSource(7))
+ for i := 0; i < 2; i++ {
+ r := rand.New(rand.NewSource(r.Int63()))
+ stop.Add(1)
+ go func() {
+ for i := 0; i < 1000; i++ {
+ fset.Position(Pos(r.Int31n(max)))
+ }
+ stop.Done()
+ }()
+ }
+ stop.Wait()
+}
コアとなるコードの解説
追加された TestFileSetRace
関数は以下のステップで構成されています。
-
パッケージのインポート:
"math/rand"
: 乱数生成のためにインポートされます。"sync"
: 並行処理の同期のためにsync.WaitGroup
を使用するためにインポートされます。
-
TestFileSetRace
関数の定義:func TestFileSetRace(t *testing.T)
: Goのテスト関数として定義されています。関数名のコメント// issue 4345. Test concurrent use of FileSet.Pos does not trigger a // race in the FileSet position cache.
は、このテストがIssue 4345(実際にはコミットメッセージでは #4354 と記載されているが、テストコードのコメントでは #4345 となっている。これはよくある誤記か、関連する別のIssue番号の可能性もある)に関連し、FileSet
の位置キャッシュにおける競合状態をトリガーしないことを確認するためのものであることを示しています。
-
FileSet
の初期化とファイルの追加:fset := NewFileSet()
: 新しいFileSet
インスタンスを作成します。for i := 0; i < 100; i++ { fset.AddFile(fmt.Sprintf("file-%d", i), fset.Base(), 1031) }
: 100個のダミーファイル(file-0
からfile-99
)をFileSet
に追加します。各ファイルはfset.Base()
から始まるオフセットを持ち、サイズは1031バイトです。これにより、FileSet
の内部キャッシュに多くのエントリが作成され、並行アクセス時の競合の可能性が高まります。
-
最大オフセットの計算:
max := int32(fset.Base())
:fset.Base()
は、現在FileSet
に追加されている全てのファイルの合計サイズ(バイトオフセット)を返します。これをint32
にキャストしてmax
変数に格納します。これは、fset.Position
に渡すPos
値の最大範囲として使用されます。
-
WaitGroup
の初期化:var stop sync.WaitGroup
: 複数のゴルーチンの完了を待つためのsync.WaitGroup
を宣言します。
-
乱数ジェネレータの初期化:
r := rand.New(rand.NewSource(7))
: メインの乱数ジェネレータをシード値7
で初期化します。この乱数ジェネレータは、各ゴルーチンに異なるシード値を与えるために使用されます。
-
並行ゴルーチンの起動:
for i := 0; i < 2; i++ { ... }
: 2つのゴルーチンを起動します。r := rand.New(rand.NewSource(r.Int63()))
: 各ゴルーチン内で、メインの乱数ジェネレータから取得した新しいシード値 (r.Int63()
) を使って、そのゴルーチン専用の乱数ジェネレータを作成します。これにより、各ゴルーチンが独立した乱数シーケンスを生成し、FileSet.Pos
に異なるPos
値を問い合わせることを保証します。stop.Add(1)
: 各ゴルーチンが起動する前に、WaitGroup
のカウンタを1増やします。go func() { ... }()
: 匿名関数を新しいゴルーチンとして実行します。for i := 0; i < 1000; i++ { fset.Position(Pos(r.Int31n(max))) }
: 各ゴルーチンは1000回ループし、ランダムに生成されたPos
値 (r.Int31n(max)
) を使ってfset.Position
メソッドを呼び出します。この部分が、FileSet
の内部キャッシュに対する並行アクセスをシミュレートし、競合状態を誘発します。stop.Done()
: 各ゴルーチンがループを完了した後、WaitGroup
のカウンタを1減らします。
-
ゴルーチンの完了待機:
stop.Wait()
: メインゴルーチンは、WaitGroup
のカウンタがゼロになるまで(つまり、両方のゴルーチンがstop.Done()
を呼び出すまで)ブロックします。これにより、テストが全ての並行処理の完了を待ってから終了することが保証されます。
このテストは、go test -race
コマンドで実行された際に、FileSet.Pos
の実装に競合状態があれば、Goのレース検出器がそれを報告するように設計されています。テストがエラーなく完了すれば、競合状態が存在しないか、少なくともこのテストケースでは検出されないことを意味します。
関連リンク
- Go Issue #4354: https://github.com/golang/go/issues/4354 (コミットメッセージに記載されているIssue番号)
- Go CL 6940078: https://golang.org/cl/6940078 (Gerrit Code Reviewへのリンク)
参考にした情報源リンク
- Go言語公式ドキュメント:
go/token
パッケージ - Go言語公式ドキュメント:
sync
パッケージ - Go言語公式ドキュメント:
math/rand
パッケージ - Go言語における競合状態の検出 (
go test -race
): - Go言語における並行処理と同期の基本概念に関する一般的な情報源。