[インデックス 14691] ファイルの概要
このコミットは、Go言語のgo/token
パッケージにおけるFileSet.last
フィールドのデータ競合(data race)を修正し、関連するテストを追加するものです。go/token
パッケージは、Goのソースコードを解析する際に、ファイル内の位置情報(行番号、列番号、オフセットなど)を管理するために使用されます。
コミット
commit 07e706f8ce98e3701998afdc18bb62dac324fb2a
Author: Dave Cheney <dave@cheney.net>
Date: Thu Dec 20 08:26:24 2012 +1100
go/token: fix data race on FileSet.last
Fixes #4345.
Benchmarks are promising,
benchmark old ns/op new ns/op delta
BenchmarkPrint 14716391 14747131 +0.21%
benchmark old ns/op new ns/op delta
BenchmarkParse 8846219 8809343 -0.42%
benchmark old MB/s new MB/s speedup
BenchmarkParse 6.61 6.64 1.00x
Also includes additional tests to improve token.FileSet coverage.
R=dvyukov, gri
CC=golang-dev
https://golang.org/cl/6968044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/07e706f8ce98e3701998afdc18bb62dac324fb2a
元コミット内容
go/token: fix data race on FileSet.last
Fixes #4345.
Benchmarks are promising,
benchmark old ns/op new ns/op delta
BenchmarkPrint 14716391 14747131 +0.21%
benchmark old ns/op new ns/op delta
BenchmarkParse 8846219 8809343 -0.42%
benchmark old MB/s new MB/s speedup
BenchmarkParse 6.61 6.64 1.00x
Also includes additional tests to improve token.FileSet coverage.
R=dvyukov, gri
CC=golang-dev
https://golang.org/cl/6968044
変更の背景
このコミットは、Go言語のgo/token
パッケージ内のFileSet
構造体におけるデータ競合の問題を解決するために行われました。具体的には、FileSet
のlast
フィールドが複数のゴルーチンから同時にアクセスされた際に、適切な同期メカニズムがないために発生する競合状態が問題でした。
FileSet
は、Goのコンパイラやツールがソースコードの位置情報を効率的に管理するためのデータ構造です。last
フィールドは、最後にアクセスされたFile
オブジェクトをキャッシュとして保持することで、連続する位置情報参照のパフォーマンスを向上させる目的で使用されていました。しかし、このキャッシュへの読み書きが複数のゴルーチンから非同期に行われると、予期せぬ動作やクラッシュを引き起こす可能性がありました。
コミットメッセージにあるFixes #4345
は、この問題がGoのIssueトラッカーで報告されていたことを示しています。データ競合は、並行処理において最も診断が難しく、かつ深刻なバグの一つであり、プログラムの信頼性と安定性に直接影響します。そのため、この修正はgo/token
パッケージの堅牢性を高める上で重要でした。
ベンチマーク結果も示されており、修正によるパフォーマンスへの影響が最小限であることを確認しています。BenchmarkPrint
とBenchmarkParse
の数値は、修正後もほぼ同等か、わずかに改善していることが示されています。これは、同期メカニズムの導入がオーバーヘッドをほとんど発生させていないことを意味します。
前提知識の解説
Go言語のgo/token
パッケージ
go/token
パッケージは、Go言語のソースコードを解析する際に、ファイル内の位置情報(行番号、列番号、オフセットなど)を管理するための基本的な型と関数を提供します。主な構成要素は以下の通りです。
Pos
: ソースファイル内のオフセットを表す型です。これは単なるint
のエイリアスですが、意味的な区別を明確にするために使用されます。File
: 単一のソースファイルに関する情報(ファイル名、サイズ、行オフセットなど)を保持する構造体です。FileSet
: 複数のFile
オブジェクトの集合を管理する構造体です。コンパイラやツールは、複数のソースファイルを扱う際にFileSet
を使用して、すべてのファイルにわたる一貫した位置情報を提供します。FileSet
は、Pos
から対応するFile
オブジェクトやPosition
(行番号、列番号を含む詳細な位置情報)を効率的に検索する機能を提供します。
データ競合 (Data Race)
データ競合は、並行プログラミングにおいて発生するバグの一種です。以下の3つの条件がすべて満たされた場合に発生します。
- 複数のゴルーチン(またはスレッド)が、同じメモリ位置に同時にアクセスする。
- 少なくとも1つのアクセスが書き込み操作である。
- アクセスが同期メカニズムによって保護されていない。
データ競合が発生すると、プログラムの動作が非決定論的になり、予期せぬ結果(間違った値の読み込み、プログラムのクラッシュなど)を引き起こす可能性があります。Go言語には、go run -race
フラグを使用してデータ競合を検出する「レース検出器(Race Detector)」が組み込まれており、開発者がこのような問題を特定するのに役立ちます。
Go言語における並行処理の同期
Go言語では、データ競合を防ぐためにsync
パッケージが提供されています。
sync.Mutex
: 排他ロック(mutual exclusion lock)を提供します。Lock()
メソッドでロックを取得し、Unlock()
メソッドでロックを解放します。ロックが取得されている間は、他のゴルーチンはそのロックを取得できません。主に書き込み操作を保護するために使用されます。sync.RWMutex
: 読み書きロック(reader-writer mutex)を提供します。複数の読み取り操作は同時に許可されますが、書き込み操作は排他的に行われます。RLock()
で読み取りロックを取得し、RUnlock()
で解放します。Lock()
で書き込みロックを取得し、Unlock()
で解放します。読み取りが頻繁で書き込みが少ない場合にパフォーマンスを向上させることができます。
技術的詳細
このコミットでは、go/token
パッケージのFileSet
構造体におけるlast
フィールドのデータ競合を解決するために、sync.RWMutex
が導入されています。
FileSet
構造体の変更
修正前は、FileSet
構造体には明示的な同期メカニズムがありませんでした。
type FileSet struct {
// ...
last *File // last file added or successfully looked up
// ...
}
修正後、FileSet
構造体にsync.RWMutex
が追加されました。
type FileSet struct {
// ...
mutex sync.RWMutex // protects the following fields
last *File // last file added or successfully looked up
// ...
}
このmutex
フィールドが、last
フィールドへのアクセスを保護するために使用されます。
NewFileSet
関数の変更
NewFileSet
関数は、FileSet
の初期化方法が変更されました。修正前はnew(FileSet)
でポインタを初期化していましたが、修正後は構造体リテラルを使用して初期化しています。これは機能的な変更ではなく、よりGoらしい(idiomatic)初期化方法への変更です。
--- a/src/pkg/go/token/position.go
+++ b/src/pkg/go/token/position.go
@@ -295,9 +295,9 @@ type FileSet struct {
// NewFileSet creates a new file set.
func NewFileSet() *FileSet {
- s := new(FileSet)
- s.base = 1 // 0 == NoPos
- return s
+ return &FileSet{
+ base: 1, // 0 == NoPos
+ }
}
FileSet.file
メソッドの変更
FileSet.file
メソッドは、Pos
(位置情報)に対応するFile
オブジェクトを検索する内部ヘルパー関数です。このメソッド内でlast
フィールドへのアクセスが行われるため、データ競合の主要な発生源となっていました。
修正では、s.mutex.RLock()
とs.mutex.RUnlock()
を使用して読み取りロックを取得・解放し、s.last
への読み取りアクセスを保護しています。
特に注目すべきは、s.last = f
という行です。この行は、検索によって見つかったFile
オブジェクトをlast
キャッシュに書き込む部分です。この書き込みは、s.mutex.Lock()
とs.mutex.Unlock()
によって書き込みロックで保護されています。
しかし、コミットメッセージのコメントにはrace is ok - s.last is only a cache
とあります。これは、s.last
が単なるパフォーマンス最適化のためのキャッシュであり、その値が一時的に不整合であってもプログラムの正確性には影響しない、という設計上の判断を示しています。つまり、複数のゴルーチンが同時にs.last
を更新しようとしても、最終的な結果(正しいFile
オブジェクトの取得)には影響がなく、単にキャッシュのヒット率が一時的に低下する可能性があるだけ、という考え方です。このため、RLock
の範囲外でLock
を取得し、キャッシュの更新を行っています。これにより、読み取り操作の大部分をブロックすることなく、キャッシュの更新を保護しています。
--- a/src/pkg/go/token/position.go
+++ b/src/pkg/go/token/position.go
@@ -367,8 +367,10 @@ func searchFiles(a []*File, x int) int {
}
func (s *FileSet) file(p Pos) *File {
+ s.mutex.RLock()
// common case: p is in last file
if f := s.last; f != nil && f.base <= int(p) && int(p) <= f.base+f.size {
+ s.mutex.RUnlock()
return f
}
// p is not in last file - search all files
i := searchFiles(s.files, int(p))
if 0 <= i && i < len(s.files) {
f := s.files[i]
// f.base <= int(p) by definition of searchFiles
if int(p) <= f.base+f.size {
- s.last = f
+ s.mutex.RUnlock()
+ s.mutex.Lock()
+ s.last = f // race is ok - s.last is only a cache
+ s.mutex.Unlock()
return f
}
}
+ s.mutex.RUnlock()
return nil
}
FileSet.File
およびFileSet.Position
メソッドの変更
これらの公開メソッドは、内部でFileSet.file
を呼び出しています。修正前はこれらのメソッド内でRLock
/RUnlock
を行っていましたが、FileSet.file
メソッド自体がロックを管理するようになったため、これらのメソッドからはロック関連のコードが削除されました。これにより、ロックの責務がFileSet.file
に集約され、コードの簡潔性と保守性が向上しています。
--- a/src/pkg/go/token/position.go
+++ b/src/pkg/go/token/position.go
@@ -389,9 +395,7 @@ func (s *FileSet) file(p Pos) *File {
//
func (s *FileSet) File(p Pos) (f *File) {
if p != NoPos {
- s.mutex.RLock()
f = s.file(p)
- s.mutex.RUnlock()
}
return
}
@@ -399,11 +403,9 @@ func (s *FileSet) File(p Pos) (f *File) {
// Position converts a Pos in the fileset into a general Position.
func (s *FileSet) Position(p Pos) (pos Position) {
if p != NoPos {
- s.mutex.RLock()
if f := s.file(p); f != nil {
pos = f.position(p)
}
- s.mutex.RUnlock()
}
return
}
テストの追加
データ競合の修正だけでなく、token.FileSet
のテストカバレッジを向上させるための追加テストも含まれています。
TestFileSetPastEnd
:FileSet
の範囲外のPos
が与えられた場合にFileSet.File
がnil
を返すことを確認するテスト。TestFileSetCacheUnlikely
:FileSet
のキャッシュが正しく機能することを確認するテスト。TestFileSetRace
:FileSet.Pos
の並行使用がデータ競合を引き起こさないことを確認するテスト。これは、実際に複数のゴルーチンを起動してFileSet
にアクセスさせ、レース検出器が競合を報告しないことを検証するものです。
これらのテストは、修正が正しく機能していることを保証し、将来的なリグレッションを防ぐ上で非常に重要です。
コアとなるコードの変更箇所
diff --git a/src/pkg/go/token/position.go b/src/pkg/go/token/position.go
index fc45c1e769..f5d9995618 100644
--- a/src/pkg/go/token/position.go
+++ b/src/pkg/go/token/position.go
@@ -295,9 +295,9 @@ type FileSet struct {
// NewFileSet creates a new file set.
func NewFileSet() *FileSet {
- s := new(FileSet)
- s.base = 1 // 0 == NoPos
- return s
+ return &FileSet{
+ base: 1, // 0 == NoPos
+ }
}
// Base returns the minimum base offset that must be provided to
@@ -367,8 +367,10 @@ func searchFiles(a []*File, x int) int {
}
func (s *FileSet) file(p Pos) *File {
+ s.mutex.RLock()
// common case: p is in last file
if f := s.last; f != nil && f.base <= int(p) && int(p) <= f.base+f.size {
+ s.mutex.RUnlock()
return f
}
// p is not in last file - search all files
i := searchFiles(s.files, int(p))
if 0 <= i && i < len(s.files) {
f := s.files[i]
// f.base <= int(p) by definition of searchFiles
if int(p) <= f.base+f.size {
- s.last = f
+ s.mutex.RUnlock()
+ s.mutex.Lock()
+ s.last = f // race is ok - s.last is only a cache
+ s.mutex.Unlock()
return f
}
}
+ s.mutex.RUnlock()
return nil
}
@@ -389,9 +395,7 @@ func (s *FileSet) file(p Pos) *File {
//
func (s *FileSet) File(p Pos) (f *File) {
if p != NoPos {
- s.mutex.RLock()
f = s.file(p)
- s.mutex.RUnlock()
}
return
}
@@ -399,11 +403,9 @@ func (s *FileSet) File(p Pos) (f *File) {
// Position converts a Pos in the fileset into a general Position.
func (s *FileSet) Position(p Pos) (pos Position) {
if p != NoPos {
- s.mutex.RLock()
if f := s.file(p); f != nil {
pos = f.position(p)
}
- s.mutex.RUnlock()
}
return
}
diff --git a/src/pkg/go/token/position_test.go b/src/pkg/go/token/position_test.go
index 3e7d552b75..1d36c22268 100644
--- a/src/pkg/go/token/position_test.go
+++ b/src/pkg/go/token/position_test.go
@@ -182,6 +182,32 @@ func TestFiles(t *testing.T) {
}
}\n
+// FileSet.File should return nil if Pos is past the end of the FileSet.
+func TestFileSetPastEnd(t *testing.T) {\n+\tfset := NewFileSet()\n+\tfor _, test := range tests {\n+\t\tfset.AddFile(test.filename, fset.Base(), test.size)\n+\t}\n+\tif f := fset.File(Pos(fset.Base())); f != nil {\n+\t\tt.Errorf("expected nil, got %v", f)\n+\t}\n+}\n+\n+func TestFileSetCacheUnlikely(t *testing.T) {\n+\tfset := NewFileSet()\n+\toffsets := make(map[string]int)\n+\tfor _, test := range tests {\n+\t\toffsets[test.filename] = fset.Base()\n+\t\tfset.AddFile(test.filename, fset.Base(), test.size)\n+\t}\n+\tfor file, pos := range offsets {\n+\t\tf := fset.File(Pos(pos))\n+\t\tif f.Name() != file {\n+\t\t\tt.Errorf("expecting %q at position %d, got %q", file, pos, f.Name())\n+\t\t}\n+\t}\n+}\n+\n // issue 4345. Test concurrent use of FileSet.Pos does not trigger a\n // race in the FileSet position cache.\n func TestFileSetRace(t *testing.T) {\n```
## コアとなるコードの解説
### `src/pkg/go/token/position.go`
1. **`FileSet`構造体への`mutex`フィールドの追加**:
`FileSet`構造体に`sync.RWMutex`型の`mutex`フィールドが追加されました。これにより、`FileSet`の内部状態、特に`last`フィールドへの並行アクセスを安全に制御できるようになります。
2. **`NewFileSet`関数の変更**:
`NewFileSet`関数は、`FileSet`の初期化方法を`new(FileSet)`から構造体リテラル`&FileSet{base: 1}`に変更しました。これは機能的な変更ではなく、よりGoらしい初期化スタイルへの変更です。
3. **`FileSet.file`メソッドの変更**:
* メソッドの冒頭で`s.mutex.RLock()`が呼び出され、読み取りロックが取得されます。これにより、`last`フィールドの読み取りが保護されます。
* `last`キャッシュがヒットした場合(`if f := s.last; ...`の条件が真の場合)、`s.mutex.RUnlock()`が呼び出され、読み取りロックが解放されてから`f`が返されます。
* `last`キャッシュがヒットせず、ファイルが検索によって見つかった場合(`if int(p) <= f.base+f.size { ... }`のブロック内):
* まず、現在の読み取りロック`s.mutex.RUnlock()`が解放されます。
* 次に、`s.mutex.Lock()`が呼び出され、書き込みロックが取得されます。これは、`s.last = f`という書き込み操作を保護するためです。
* `s.last = f`でキャッシュが更新されます。コメント`// race is ok - s.last is only a cache`は、この特定の書き込み操作が、厳密な排他制御がなくても許容される性質のものであることを示唆しています。これは、キャッシュの更新がアトミックでなくても、最終的な結果の正確性には影響しないためです。
* 最後に、`s.mutex.Unlock()`が呼び出され、書き込みロックが解放されます。
* メソッドの終わりに`return nil`の前に`s.mutex.RUnlock()`が追加され、すべてのパスで読み取りロックが確実に解放されるようにしています。
4. **`FileSet.File`および`FileSet.Position`メソッドの変更**:
これらのメソッドから`s.mutex.RLock()`と`s.mutex.RUnlock()`の呼び出しが削除されました。これは、内部で呼び出される`FileSet.file`メソッドがすでに適切なロックを管理しているため、冗長なロックを避けるためです。これにより、ロックの責務が`FileSet.file`に集約され、コードがよりクリーンになりました。
### `src/pkg/go/token/position_test.go`
1. **`TestFileSetPastEnd`関数の追加**:
`FileSet.File`メソッドが、`FileSet`の範囲外の`Pos`に対して`nil`を正しく返すことを検証するテストです。
2. **`TestFileSetCacheUnlikely`関数の追加**:
`FileSet`のキャッシュメカニズムが、異なるファイルへのアクセスパターンでも正しく機能し、期待される`File`オブジェクトを返すことを検証するテストです。
3. **`TestFileSetRace`関数の追加**:
このテストは、Issue #4345で報告されたデータ競合の問題を具体的に検証するために追加されました。複数のゴルーチンを起動し、同時に`FileSet.Pos`メソッドを呼び出すことで、並行アクセス下での`FileSet`の動作をシミュレートします。このテストがレース検出器によって競合を報告しないことを確認することで、修正がデータ競合を効果的に解決したことを保証します。
これらの変更により、`go/token`パッケージの`FileSet`は、並行環境下でも安全かつ効率的に動作するようになりました。
## 関連リンク
* GitHubコミットページ: [https://github.com/golang/go/commit/07e706f8ce98e3701998afdc18bb62dac324fb2a](https://github.com/golang/go/commit/07e706f8ce98e3701998afdc18bb62dac324fb2a)
* Go CL (Change List): [https://golang.org/cl/6968044](https://golang.org/cl/6968044)
## 参考にした情報源リンク
* Go言語の`sync`パッケージに関する公式ドキュメント: [https://pkg.go.dev/sync](https://pkg.go.dev/sync)
* Go言語の`go/token`パッケージに関する公式ドキュメント: [https://pkg.go.dev/go/token](https://pkg.go.dev/go/token)
* Go言語におけるデータ競合の検出と回避に関する情報(Go公式ブログなど)
* The Go Race Detector: [https://go.dev/blog/race-detector](https://go.dev/blog/race-detector)
* 一般的なデータ競合の概念に関する情報(並行プログラミングの教科書など)