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

[インデックス 14690] ファイルの概要

このコミットは、Go言語の標準ライブラリの一部である go/token パッケージ内の position_test.go ファイルに対する変更です。go/token パッケージは、Goソースコードの字句解析(lexing)や構文解析(parsing)において、ファイル名、行番号、列番号といったソースコード上の位置情報を管理するための重要な機能を提供します。position_test.go は、このパッケージの機能、特に FileSetPos といった位置情報管理に関連する機能の正確性と堅牢性を検証するためのテストコードを含んでいます。

コミット

このコミットは、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 メソッドが内部的に持つ位置キャッシュの競合状態を意図的に引き起こすように設計されています。

  1. FileSet の準備: まず、NewFileSet() で新しい FileSet インスタンスを作成します。
  2. 多数のダミーファイルの追加: for i := 0; i < 100; i++ { fset.AddFile(fmt.Sprintf("file-%d", i), fset.Base(), 1031) } のループにより、100個のダミーファイルが FileSet に追加されます。これにより、FileSet の内部キャッシュが多くの位置情報で初期化され、競合状態が発生しやすい環境が作られます。fset.Base() は、新しいファイルの開始オフセットを決定するために使用され、各ファイルが異なるオフセット範囲を持つようにします。
  3. 最大オフセットの取得: max := int32(fset.Base()) で、FileSet が管理するオフセットの最大値を取得します。これは、後でランダムな Pos 値を生成する際の範囲として使用されます。
  4. 並行処理のセットアップ:
    • var stop sync.WaitGroupWaitGroup を初期化します。
    • for i := 0; i < 2; i++ のループで2つのゴルーチンを起動します。
    • 各ゴルーチンは stop.Add(1)WaitGroup のカウンタを増やします。
    • go func() { ... }() で匿名関数をゴルーチンとして実行します。
  5. 競合の誘発:
    • 各ゴルーチン内で 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 の内部キャッシュに対して大量の並行読み取りアクセスが発生し、競合状態が露呈する可能性が高まります。
  6. ゴルーチンの完了待機: 各ゴルーチンは処理が完了すると 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 関数は以下のステップで構成されています。

  1. パッケージのインポート:

    • "math/rand": 乱数生成のためにインポートされます。
    • "sync": 並行処理の同期のために sync.WaitGroup を使用するためにインポートされます。
  2. 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 の位置キャッシュにおける競合状態をトリガーしないことを確認するためのものであることを示しています。
  3. 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 の内部キャッシュに多くのエントリが作成され、並行アクセス時の競合の可能性が高まります。
  4. 最大オフセットの計算:

    • max := int32(fset.Base()): fset.Base() は、現在 FileSet に追加されている全てのファイルの合計サイズ(バイトオフセット)を返します。これを int32 にキャストして max 変数に格納します。これは、fset.Position に渡す Pos 値の最大範囲として使用されます。
  5. WaitGroup の初期化:

    • var stop sync.WaitGroup: 複数のゴルーチンの完了を待つための sync.WaitGroup を宣言します。
  6. 乱数ジェネレータの初期化:

    • r := rand.New(rand.NewSource(7)): メインの乱数ジェネレータをシード値 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減らします。
  8. ゴルーチンの完了待機:

    • stop.Wait(): メインゴルーチンは、WaitGroup のカウンタがゼロになるまで(つまり、両方のゴルーチンが stop.Done() を呼び出すまで)ブロックします。これにより、テストが全ての並行処理の完了を待ってから終了することが保証されます。

このテストは、go test -race コマンドで実行された際に、FileSet.Pos の実装に競合状態があれば、Goのレース検出器がそれを報告するように設計されています。テストがエラーなく完了すれば、競合状態が存在しないか、少なくともこのテストケースでは検出されないことを意味します。

関連リンク

参考にした情報源リンク