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

[インデックス 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構造体におけるデータ競合の問題を解決するために行われました。具体的には、FileSetlastフィールドが複数のゴルーチンから同時にアクセスされた際に、適切な同期メカニズムがないために発生する競合状態が問題でした。

FileSetは、Goのコンパイラやツールがソースコードの位置情報を効率的に管理するためのデータ構造です。lastフィールドは、最後にアクセスされたFileオブジェクトをキャッシュとして保持することで、連続する位置情報参照のパフォーマンスを向上させる目的で使用されていました。しかし、このキャッシュへの読み書きが複数のゴルーチンから非同期に行われると、予期せぬ動作やクラッシュを引き起こす可能性がありました。

コミットメッセージにあるFixes #4345は、この問題がGoのIssueトラッカーで報告されていたことを示しています。データ競合は、並行処理において最も診断が難しく、かつ深刻なバグの一つであり、プログラムの信頼性と安定性に直接影響します。そのため、この修正はgo/tokenパッケージの堅牢性を高める上で重要でした。

ベンチマーク結果も示されており、修正によるパフォーマンスへの影響が最小限であることを確認しています。BenchmarkPrintBenchmarkParseの数値は、修正後もほぼ同等か、わずかに改善していることが示されています。これは、同期メカニズムの導入がオーバーヘッドをほとんど発生させていないことを意味します。

前提知識の解説

Go言語のgo/tokenパッケージ

go/tokenパッケージは、Go言語のソースコードを解析する際に、ファイル内の位置情報(行番号、列番号、オフセットなど)を管理するための基本的な型と関数を提供します。主な構成要素は以下の通りです。

  • Pos: ソースファイル内のオフセットを表す型です。これは単なるintのエイリアスですが、意味的な区別を明確にするために使用されます。
  • File: 単一のソースファイルに関する情報(ファイル名、サイズ、行オフセットなど)を保持する構造体です。
  • FileSet: 複数のFileオブジェクトの集合を管理する構造体です。コンパイラやツールは、複数のソースファイルを扱う際にFileSetを使用して、すべてのファイルにわたる一貫した位置情報を提供します。FileSetは、Posから対応するFileオブジェクトやPosition(行番号、列番号を含む詳細な位置情報)を効率的に検索する機能を提供します。

データ競合 (Data Race)

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

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

データ競合が発生すると、プログラムの動作が非決定論的になり、予期せぬ結果(間違った値の読み込み、プログラムのクラッシュなど)を引き起こす可能性があります。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.Filenilを返すことを確認するテスト。
  • 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)
*   一般的なデータ競合の概念に関する情報(並行プログラミングの教科書など)