[インデックス 16581] ファイルの概要
このコミットは、Goランタイムのデータ競合検出器(Race Detector)に関連するテストケースを追加するものです。具体的には、構造体の埋め込みフィールドに対するメソッド呼び出し(いわゆる「メソッドサンク」)がデータ競合を引き起こす可能性のあるシナリオと、そうでないシナリオを検証するためのテストがsrc/pkg/runtime/race/testdata/mop_test.go
に追加されています。
コミット
commit d660688f14baf29a84d1d7cbeea9eae92c15c60f
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Jun 17 17:59:53 2013 +0400
runtime/race: add tests for method thunks
R=golang-dev, dave
CC=golang-dev
https://golang.org/cl/10257043
---
src/pkg/runtime/race/testdata/mop_test.go | 87 +++++++++++++++++++++++++++++++
1 file changed, 87 insertions(+)
diff --git a/src/pkg/runtime/race/testdata/mop_test.go b/src/pkg/runtime/race/testdata/mop_test.go
index 9a0cb81f53..b5eecd9fdf 100644
--- a/src/pkg/runtime/race/testdata/mop_test.go
+++ b/src/pkg/runtime/race/testdata/mop_test.go
@@ -1811,3 +1811,90 @@ And Brutus is an honourable man.`
}\n _ = res\n }\n+\n+type Base int\n+\n+func (b *Base) Foo() int {\n+\treturn 42\n+}\n+\n+func (b Base) Bar() int {\n+\treturn int(b)\n+}\n+\n+func TestNoRaceMethodThunk(t *testing.T) {\n+\ttype Derived struct {\n+\t\tpad int\n+\t\tBase\n+\t}\n+\tvar d Derived\n+\tdone := make(chan bool)\n+\tgo func() {\n+\t\t_ = d.Foo()\n+\t\tdone <- true\n+\t}()\n+\td = Derived{}\n+\t<-done\n+}\n+\n+func TestRaceMethodThunk(t *testing.T) {\n+\ttype Derived struct {\n+\t\tpad int\n+\t\t*Base\n+\t}\n+\tvar d Derived\n+\tdone := make(chan bool)\n+\tgo func() {\n+\t\t_ = d.Foo()\n+\t\tdone <- true\t}()\n+\td = Derived{}\n+\t<-done\n+}\n+\n+func TestRaceMethodThunk2(t *testing.T) {\n+\ttype Derived struct {\n+\t\tpad int\n+\t\tBase\n+\t}\n+\tvar d Derived\n+\tdone := make(chan bool)\n+\tgo func() {\n+\t\t_ = d.Bar()\n+\t\tdone <- true\n+\t}()\n+\td = Derived{}\n+\t<-done\n+}\n+\n+func TestRaceMethodThunk3(t *testing.T) {\n+\ttype Derived struct {\n+\t\tpad int\n+\t\t*Base\n+\t}\n+\tvar d Derived\n+\td.Base = new(Base)\n+\tdone := make(chan bool)\n+\tgo func() {\n+\t\t_ = d.Bar()\n+\t\tdone <- true\n+\t}()\n+\td.Base = new(Base)\n+\t<-done\n+}\n+\n+func TestRaceMethodThunk4(t *testing.T) {\n+\ttype Derived struct {\n+\t\tpad int\n+\t\t*Base\n+\t}\n+\tvar d Derived\n+\td.Base = new(Base)\n+\tdone := make(chan bool)\n+\tgo func() {\n+\t\t_ = d.Bar()\n+\t\tdone <- true\n+\t}()\n+\t*(*int)(d.Base) = 42\n+\t<-done\n+}\n```
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/d660688f14baf29a84d1d7cbeea9eae92c15c60f](https://github.com/golang/go/commit/d660688f14baf29a84d1d7cbeea9eae92c15c60f)
## 元コミット内容
runtime/race: add tests for method thunks
R=golang-dev, dave
CC=golang-dev
https://golang.org/cl/10257043
## 変更の背景
Go言語のデータ競合検出器(Race Detector)は、並行処理におけるデータ競合を検出するための強力なツールです。しかし、Goの言語機能の一つである「構造体の埋め込み(embedding)」と、それに伴う「メソッドサンク(method thunks)」の挙動は、通常の変数アクセスとは異なる形でメモリにアクセスする可能性があります。
このコミットが作成された背景には、GoのRace Detectorが、構造体の埋め込みによって生成されるメソッドサンクを介したメモリアクセスに対して、適切にデータ競合を検出できるかどうかの検証が必要だったという点があります。特に、ポインタレシーバを持つメソッドと値レシーバを持つメソッド、そして埋め込みフィールドが値型かポインタ型かによって、メモリアクセスのセマンティクスが変化するため、これらの組み合わせがデータ競合検出に与える影響を網羅的にテストする必要がありました。
このテストの追加は、Race Detectorの堅牢性を高め、Goプログラムの並行処理における潜在的なバグをより確実に特定できるようにすることを目的としています。
## 前提知識の解説
### 1. Goのデータ競合検出器 (Race Detector)
GoのRace Detectorは、Goプログラムの実行中に発生するデータ競合(data race)を検出するためのツールです。データ競合とは、複数のGoroutineが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生するバグです。データ競合は、プログラムの予測不能な動作、クラッシュ、または誤った結果を引き起こす可能性があります。
Race Detectorは、プログラムの実行を監視し、メモリアクセスとGoroutineのスケジューリングに関する情報を収集します。そして、これらの情報に基づいて、データ競合のパターンを特定します。Goプログラムは、`go run -race`、`go build -race`、`go test -race`などのコマンドでビルドまたは実行することで、Race Detectorを有効にできます。
### 2. Goの構造体の埋め込み (Struct Embedding)
Go言語では、構造体の中に別の構造体やインターフェースを「埋め込む」ことができます。これは、他の言語における継承に似た機能を提供しますが、Goでは「コンポジション(合成)」として扱われます。埋め込まれた型は、そのフィールドやメソッドを外側の構造体に「昇格(promote)」させます。これにより、外側の構造体のインスタンスから、埋め込まれた型のフィールドやメソッドに直接アクセスできるようになります。
例:
```go
type Base struct {
Value int
}
func (b Base) GetValue() int {
return b.Value
}
type Derived struct {
Base // Base構造体を埋め込み
AnotherField string
}
func main() {
d := Derived{Base: Base{Value: 10}, AnotherField: "hello"}
fmt.Println(d.Value) // Base.Valueに直接アクセス
fmt.Println(d.GetValue()) // Base.GetValue()に直接アクセス
}
3. メソッドサンク (Method Thunks)
構造体の埋め込みにおいて、埋め込まれた型のメソッドが外側の構造体から呼び出される際、コンパイラは「メソッドサンク」と呼ばれる小さなラッパー関数を生成することがあります。これは、外側の構造体のインスタンスから埋め込まれた型のインスタンスへのポインタを調整し、適切なレシーバでメソッドを呼び出すためのものです。
特に、埋め込まれたフィールドがポインタ型である場合や、レシーバの型が異なる場合(例:値レシーバのメソッドをポインタレシーバのインスタンスから呼び出す場合など)に、このサンクが生成されることがあります。このサンクは、元のメソッドが期待するレシーバの型に合わせて、レシーバの値を変換したり、ポインタをデリファレンスしたりする役割を担います。
メソッドサンクは、Goのコンパイラが内部的に生成するものであり、開発者が直接意識することは通常ありません。しかし、Race Detectorのようなツールにとっては、このサンクがどのようにメモリにアクセスするかが重要になります。サンクが生成される過程で、元の埋め込みフィールドへのアクセスが発生するため、そのアクセスが並行処理下でデータ競合を引き起こす可能性があるからです。
技術的詳細
このコミットで追加されたテストケースは、GoのRace Detectorがメソッドサンクを介したメモリアクセスを正しく処理できるかを検証しています。テストは、Base
型とDerived
型を定義し、Derived
型がBase
型を埋め込む形で構成されています。Base
型には、ポインタレシーバのメソッドFoo()
と値レシーバのメソッドBar()
があります。
テストの主要なポイントは以下の通りです。
-
Base
型の定義:type Base int
:Base
型はint
のエイリアスとして定義されています。func (b *Base) Foo() int
:Base
型へのポインタをレシーバとするメソッド。func (b Base) Bar() int
:Base
型の値をレシーバとするメソッド。
-
Derived
型の定義:type Derived struct { pad int; Base }
:Base
型を値として埋め込むケース。type Derived struct { pad int; *Base }
:Base
型へのポインタを埋め込むケース。
-
テストシナリオ: 各テスト関数は、
Derived
型のインスタンスをGoroutine間で共有し、一方のGoroutineで埋め込みフィールドのメソッドを呼び出し、もう一方のGoroutineでDerived
インスタンスまたは埋め込みフィールドにアクセスすることで、データ競合が発生するかどうかを検証しています。-
TestNoRaceMethodThunk
:Derived
がBase
を値として埋め込みます。- Goroutine内で
d.Foo()
(ポインタレシーバのメソッド)を呼び出します。 - メインGoroutineで
d = Derived{}
とDerived
インスタンス全体を書き換えます。 - このケースでは、
d.Foo()
の呼び出しはd
のコピーに対して行われるため、d
自体の書き換えとは競合しないと期待されます。Race Detectorが競合を報告しないことを確認します。
-
TestRaceMethodThunk
:Derived
が*Base
をポインタとして埋め込みます。- Goroutine内で
d.Foo()
(ポインタレシーバのメソッド)を呼び出します。 - メインGoroutineで
d = Derived{}
とDerived
インスタンス全体を書き換えます。 *Base
が埋め込まれているため、d.Foo()
の呼び出しはd
が保持するポインタを介して行われます。d
の書き換えは、このポインタ自体を書き換えるため、競合が発生すると期待されます。Race Detectorが競合を報告することを確認します。
-
TestRaceMethodThunk2
:Derived
がBase
を値として埋め込みます。- Goroutine内で
d.Bar()
(値レシーバのメソッド)を呼び出します。 - メインGoroutineで
d = Derived{}
とDerived
インスタンス全体を書き換えます。 d.Bar()
はd
のコピーに対して呼び出されるため、d
自体の書き換えとは競合しないと期待されます。Race Detectorが競合を報告しないことを確認します。
-
TestRaceMethodThunk3
:Derived
が*Base
をポインタとして埋め込みます。- Goroutine内で
d.Bar()
(値レシーバのメソッド)を呼び出します。 - メインGoroutineで
d.Base = new(Base)
と埋め込みポインタを書き換えます。 d.Bar()
の呼び出しは、d.Base
が指す値にアクセスします。メインGoroutineでのd.Base
の書き換えは、このポインタ自体を書き換えるため、競合が発生すると期待されます。Race Detectorが競合を報告することを確認します。
-
TestRaceMethodThunk4
:Derived
が*Base
をポインタとして埋め込みます。- Goroutine内で
d.Bar()
(値レシーバのメソッド)を呼び出します。 - メインGoroutineで
*(*int)(d.Base) = 42
と、埋め込みポインタが指す先の値を書き換えます。 d.Bar()
の呼び出しは、d.Base
が指す値(int
型)を読み取ります。メインGoroutineでのその値への書き込みは、競合が発生すると期待されます。Race Detectorが競合を報告することを確認します。
-
これらのテストは、Goのコンパイラが生成するメソッドサンクが、埋め込みフィールドの型(値型かポインタ型か)や、呼び出されるメソッドのレシーバの型(値レシーバかポインタレシーバか)に応じて、どのようにメモリにアクセスするかを網羅的に検証しています。これにより、Race Detectorがこれらの複雑なシナリオにおいても、データ競合を正確に検出できることが保証されます。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/race/testdata/mop_test.go
ファイルに集中しており、以下の新しいテスト関数と補助的な型定義が追加されています。
type Base int
func (b *Base) Foo() int {
return 42
}
func (b Base) Bar() int {
return int(b)
}
func TestNoRaceMethodThunk(t *testing.T) {
type Derived struct {
pad int
Base
}
var d Derived
done := make(chan bool)
go func() {
_ = d.Foo()
done <- true
}()
d = Derived{}
<-done
}
func TestRaceMethodThunk(t *testing.T) {
type Derived struct {
pad int
*Base
}
var d Derived
done := make(chan bool)
go func() {
_ = d.Foo()
done <- true
}()
d = Derived{}
<-done
}
func TestRaceMethodThunk2(t *testing.T) {
type Derived struct {
pad int
Base
}
var d Derived
done := make(chan bool)
go func() {
_ = d.Bar()
done <- true
}()
d = Derived{}
<-done
}
func TestRaceMethodThunk3(t *testing.T) {
type Derived struct {
pad int
*Base
}
var d Derived
d.Base = new(Base)
done := make(chan bool)
go func() {
_ = d.Bar()
done <- true
}()
d.Base = new(Base)
<-done
}
func TestRaceMethodThunk4(t *testing.T) {
type Derived struct {
pad int
*Base
}
var d Derived
d.Base = new(Base)
done := make(chan bool)
go func() {
_ = d.Bar()
done <- true
}()
*(*int)(d.Base) = 42
<-done
}
コアとなるコードの解説
追加されたコードは、GoのRace Detectorのテストスイートの一部であり、特に構造体の埋め込みとメソッドサンクに関連するデータ競合の検出能力を検証するために設計されています。
-
Base
型とメソッドFoo
,Bar
:Base int
は、基本的な整数型をラップした型です。Foo()
はポインタレシーバ*Base
を持つメソッドです。これは、メソッドがBase
型のインスタンスのポインタを受け取り、そのポインタが指す元のメモリ位置にアクセスする可能性があることを意味します。Bar()
は値レシーバBase
を持つメソッドです。これは、メソッドがBase
型のインスタンスのコピーを受け取るため、元のメモリ位置には直接アクセスしないことを意味します。
-
Derived
型と埋め込み:- 各テスト関数内で定義される
Derived
型は、pad int
フィールドと、Base
または*Base
のいずれかを埋め込みフィールドとして持ちます。 Base
を値として埋め込む場合(例:TestNoRaceMethodThunk
,TestRaceMethodThunk2
)、Derived
インスタンスはBase
型の値のコピーを直接含みます。*Base
をポインタとして埋め込む場合(例:TestRaceMethodThunk
,TestRaceMethodThunk3
,TestRaceMethodThunk4
)、Derived
インスタンスはBase
型へのポインタを含みます。このポインタが指す先のメモリは、複数のGoroutineからアクセスされる可能性があります。
- 各テスト関数内で定義される
-
テストロジック: 各テスト関数は、以下の共通パターンに従います。
Derived
型の変数d
を宣言します。done
という名前のチャネルを作成し、Goroutineの完了を同期するために使用します。- 新しいGoroutineを起動し、その中で
d
の埋め込みフィールドのメソッド(Foo
またはBar
)を呼び出します。 - メインGoroutineで、
d
のインスタンス全体を書き換えたり(d = Derived{}
)、埋め込みポインタを書き換えたり(d.Base = new(Base)
)、埋め込みポインタが指す先の値を書き換えたり(*(*int)(d.Base) = 42
)します。 done
チャネルからの受信を待ち、Goroutineの完了を待ちます。
これらのテストは、Race Detectorが、埋め込みフィールドの型とメソッドのレシーバの型の組み合わせによって発生する、微妙なデータ競合のシナリオを正確に識別できることを保証します。特に、ポインタレシーバのメソッドがポインタ埋め込みフィールドを介して呼び出される場合や、値レシーバのメソッドがポインタ埋め込みフィールドを介して呼び出され、そのポインタが指す値が別のGoroutineで変更される場合に、Race Detectorが正しく競合を報告することが期待されます。
関連リンク
- Go Race Detector: https://go.dev/doc/articles/race_detector
- Go Struct Embedding: https://go.dev/tour/methods/10
- Go CL 10257043 (このコミットの変更リスト): https://golang.org/cl/10257043
参考にした情報源リンク
- Go言語の公式ドキュメント
- Goのソースコード(特に
src/runtime/race
ディレクトリ) - GoのIssueトラッカーやメーリングリストでの議論(Race Detectorや埋め込みに関するもの)
- Goのコンパイラとランタイムに関する技術記事やブログポスト
- データ競合検出器に関する一般的なコンピュータサイエンスの知識
- Goのテストフレームワークに関する知識
- Goの並行処理プリミティブ(Goroutine, Channel)に関する知識