[インデックス 18605] ファイルの概要
このコミットは、Goランタイムのデータ競合検出器(Race Detector)に関連するファイナライザのテストにおける非決定的な失敗を修正するものです。具体的には、src/pkg/runtime/race/testdata/finalizer_test.go
ファイル内のテストコードが変更されています。
コミット
commit f6d18c5ee93c6711b12e932c79a7e1a8374c7d45
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Feb 24 18:12:46 2014 +0400
runtime/race: fix finalizer tests
After "runtime: combine small NoScan allocations" finalizers
for small objects run more non deterministically.
TestRaceFin episodically fails on my darwin/amd64.
LGTM=khr
R=golang-codereviews, khr, dave
CC=golang-codereviews
https://golang.org/cl/56970043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f6d18c5ee93c6711b12e932c79a7e1a8374c7d45
元コミット内容
このコミットの目的は、Goランタイムのデータ競合検出器のファイナライザテストを修正することです。以前のコミット「runtime: combine small NoScan allocations」によって、小さなオブジェクトのファイナライザの実行がより非決定的になったため、TestRaceFin
テストが darwin/amd64
環境で時折失敗する問題が発生していました。このコミットは、その非決定性を考慮してテストを調整し、安定させることを目指しています。
変更の背景
このコミットの背景には、Goランタイムのメモリ管理とガベージコレクション(GC)の最適化があります。特に、コミットメッセージに言及されている「runtime: combine small NoScan allocations」という変更が重要です。
Goのガベージコレクタは、ヒープ上のオブジェクトを追跡し、不要になったメモリを解放します。このプロセスを効率化するため、Goランタイムは様々な最適化を行っています。その一つが、小さなオブジェクトのアロケーションをまとめる("combine small allocations")ことです。これにより、アロケーションのオーバーヘッドが減少し、GCの効率が向上します。
しかし、「NoScan allocations」という言葉が示すように、これらの小さなオブジェクトの中には、ポインタを含まない(スキャン不要な)ものがあります。このようなオブジェクトのアロケーションをまとめることで、GCがそれらを処理するタイミングや順序が以前よりも非決定的になる可能性があります。
ファイナライザは、オブジェクトがガベージコレクションによって回収される直前に実行される関数です。オブジェクトがいつ回収されるかはGCの動作に依存するため、ファイナライザの実行タイミングも非決定的になりがちです。上記の「combine small NoScan allocations」の変更により、この非決定性がさらに増幅され、特に小さなオブジェクトに設定されたファイナライザの実行タイミングが予測しにくくなったと考えられます。
TestRaceFin
は、Goのデータ競合検出器(Race Detector)のテストスイートの一部であり、ファイナライザが絡むデータ競合のシナリオを検証することを目的としています。ファイナライザの実行タイミングが非決定的になったことで、テストが想定する競合の発生パターンが崩れ、テストが不安定に(時折失敗するように)なったのが、このコミットの直接的な原因です。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とランタイムの動作に関する知識が必要です。
-
ガベージコレクション (Garbage Collection, GC): Goは自動メモリ管理を採用しており、不要になったメモリはガベージコレクタによって自動的に解放されます。GoのGCは並行・世代別・三色マーク&スイープ方式をベースとしており、プログラムの実行と並行して動作します。GCの実行タイミングはランタイムによって制御され、プログラマが明示的に制御することは通常ありません。
-
ファイナライザ (Finalizer):
runtime.SetFinalizer
関数を使って、特定のオブジェクトがガベージコレクションによってメモリから解放される直前に実行される関数(ファイナライザ)を登録できます。ファイナライザは、ファイルハンドルやネットワーク接続などの外部リソースをクリーンアップする際に利用されることがあります。しかし、ファイナライザの実行タイミングはGCの動作に依存するため、非決定的であり、その使用は慎重に行うべきとされています。特に、ファイナライザ内で共有リソースにアクセスする場合、競合状態が発生しやすくなります。 -
Go Race Detector (データ競合検出器): Goには、プログラム実行中にデータ競合(data race)を検出するための組み込みツールであるRace Detectorがあります。データ競合とは、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生するバグです。Race Detectorは、
go run -race
やgo test -race
のように-race
フラグを付けてプログラムを実行することで有効になります。検出された競合は詳細なスタックトレースとともに報告され、デバッグに役立ちます。 -
メモリ割り当て (Memory Allocation): Goプログラムが新しいオブジェクトを作成する際、ランタイムはヒープからメモリを割り当てます。この割り当てプロセスは、パフォーマンスを最大化するために最適化されています。小さなオブジェクトのアロケーションは、まとめて処理されることで効率が向上することがあります。
-
NoScan オブジェクト: GoのGCは、ヒープ上のオブジェクトをスキャンして、到達可能なオブジェクト(まだ使われているオブジェクト)を特定します。このスキャンプロセスは、オブジェクトがポインタを含んでいる場合に必要です。しかし、
int
やstring
のようなプリミティブ型や、ポインタを含まない構造体など、ポインタを含まないオブジェクトは「NoScan」オブジェクトと呼ばれ、GCがそれらをスキャンする必要がありません。これにより、GCのオーバーヘッドを削減できます。
技術的詳細
このコミットの技術的な核心は、Goランタイムのメモリ管理の変更が、ファイナライザの実行タイミングの非決定性を増大させ、それがRace Detectorのテストに影響を与えた点にあります。
以前のコミット「runtime: combine small NoScan allocations」は、Goランタイムが小さなNoScanオブジェクト(ポインタを含まないオブジェクト)をヒープに割り当てる方法を変更しました。具体的には、これらの小さなオブジェクトをより大きなチャンクにまとめて割り当てることで、アロケーションの効率を高め、GCの負荷を軽減しました。
この最適化の結果、個々の小さなオブジェクトがいつガベージコレクションの対象となり、そのファイナライザがいつ実行されるかというタイミングが、以前よりも予測しにくくなりました。これは、複数の小さなオブジェクトが同じメモリチャンクに割り当てられ、そのチャンク全体が回収されるまでファイナライザが実行されない可能性があるためです。また、GCの実行タイミング自体も、システムの状態や他のゴルーチンの活動によって変動するため、ファイナライザの実行は本質的に非決定的です。
TestRaceFin
は、ファイナライザ内で共有変数にアクセスするシナリオをテストしており、このアクセスがメインゴルーチンからのアクセスと競合する可能性を検証しています。ファイナライザの実行タイミングが非決定的になったことで、テストが意図する競合パターンが常に発生するとは限らなくなり、結果としてテストが不安定になったと考えられます。
このコミットの修正は、テストコード内の time.Sleep(1e8)
を time.Sleep(100 * time.Millisecond)
に変更することと、new(int)
を new(string)
に変更することです。
time.Sleep
の変更は、ファイナライザが実行されるのを待つ時間を調整するためのものです。1e8
は1億ナノ秒(100ミリ秒)を意味するため、実質的な変更はありませんが、より読みやすい 100 * time.Millisecond
に変更されています。これは、ファイナライザが実行されるための十分な時間を与えることを意図しています。ファイナライザはGCが実行された後に非同期に実行されるため、GCが完了した直後にファイナライザが実行されるとは限りません。そのため、テストがファイナライザの完了を待つために一定の時間スリープすることは一般的です。
より重要な変更は、new(int)
を new(string)
に変更した点です。
int
はNoScanオブジェクトであり、そのサイズは非常に小さいです。そのため、「combine small NoScan allocations」の最適化の影響を強く受け、ファイナライザの実行タイミングが非決定的になりやすかったと考えられます。
一方、string
はGoにおいて特殊な型であり、内部的にはポインタと長さを持つ構造体です。new(string)
で作成されるのは *string
型のポインタであり、その実体は文字列のヘッダ部分です。文字列データ自体は別のメモリ領域に格納されます。string
型はポインタを含むため、NoScanオブジェクトではありません。
new(string)
に変更することで、ファイナライザが設定されるオブジェクトがNoScanオブジェクトではなくなり、以前の「combine small NoScan allocations」の最適化の影響を受けにくくなります。これにより、ファイナライザの実行タイミングが、テストが想定する競合シナリオをより確実に発生させるような挙動に戻った、あるいは少なくともテストが安定する程度には予測可能になったと考えられます。
つまり、この修正は、テストが依存していたファイナライザの実行タイミングの特性を、ランタイムの変更によって影響を受けにくいオブジェクト型に変更することで、テストの安定性を回復させたものです。
コアとなるコードの変更箇所
変更は src/pkg/runtime/race/testdata/finalizer_test.go
ファイルに集中しています。
--- a/src/pkg/runtime/race/testdata/finalizer_test.go
+++ b/src/pkg/runtime/race/testdata/finalizer_test.go
@@ -14,16 +14,16 @@ import (
func TestNoRaceFin(t *testing.T) {
c := make(chan bool)
go func() {
- x := new(int)
- runtime.SetFinalizer(x, func(x *int) {
- *x = 42
+ x := new(string)
+ runtime.SetFinalizer(x, func(x *string) {
+ *x = "foo"
})
- *x = 66
+ *x = "bar"
c <- true
}()
<-c
runtime.GC()
- time.Sleep(1e8)
+ time.Sleep(100 * time.Millisecond)
}
var finVar struct {
@@ -34,8 +34,8 @@ var finVar struct {
func TestNoRaceFinGlobal(t *testing.T) {
c := make(chan bool)
go func() {
- x := new(int)
- runtime.SetFinalizer(x, func(x *int) {
+ x := new(string)
+ runtime.SetFinalizer(x, func(x *string) {
finVar.Lock()
finVar.cnt++
finVar.Unlock()
@@ -44,7 +44,7 @@ func TestNoRaceFinGlobal(t *testing.T) {
}()
<-c
runtime.GC()
- time.Sleep(1e8)
+ time.Sleep(100 * time.Millisecond)
finVar.Lock()
finVar.cnt++
finVar.Unlock()
@@ -54,14 +54,14 @@ func TestRaceFin(t *testing.T) {
c := make(chan bool)
y := 0
go func() {
- x := new(int)
- runtime.SetFinalizer(x, func(x *int) {
+ x := new(string)
+ runtime.SetFinalizer(x, func(x *string) {
y = 42
})
c <- true
}()
<-c
runtime.GC()
- time.Sleep(1e8)
+ time.Sleep(100 * time.Millisecond)
y = 66
}
コアとなるコードの解説
このコミットでは、finalizer_test.go
内の3つのテスト関数 TestNoRaceFin
, TestNoRaceFinGlobal
, TestRaceFin
のそれぞれで、以下の2つの変更が行われています。
-
ファイナライザ対象オブジェクトの型変更:
- 変更前:
x := new(int)
- 変更後:
x := new(string)
これにより、ファイナライザが設定されるオブジェクトの型が*int
から*string
に変更されました。前述の通り、int
はNoScanオブジェクトであり、string
はポインタを含むオブジェクトです。この変更により、ファイナライザの実行タイミングが「runtime: combine small NoScan allocations」の影響を受けにくくなり、テストの非決定性が解消されたと考えられます。 また、これに伴い、ファイナライザ内で*x
に代入される値も42
や66
といったint
型の値から、"foo"
や"bar"
といったstring
型の値に変更されています。
- 変更前:
-
スリープ時間の表記変更:
- 変更前:
time.Sleep(1e8)
- 変更後:
time.Sleep(100 * time.Millisecond)
1e8
は10の8乗ナノ秒、つまり100ミリ秒を意味します。この変更は機能的なものではなく、コードの可読性を向上させるためのものです。time.Millisecond
を使用することで、意図するスリープ時間がより明確になります。これは、GCとファイナライザが非同期に動作するため、テストがファイナライザの実行を待つための十分な時間を与えることを保証するためのものです。
- 変更前:
特に TestRaceFin
は、ファイナライザ内でグローバル変数 y
を変更し、メインゴルーチンでも y
を変更することでデータ競合を意図的に発生させるテストです。ファイナライザの実行タイミングが非決定的になると、この競合が常に発生するとは限らず、テストが不安定になります。new(string)
への変更は、このテストが意図する競合シナリオをより確実に再現できるようにするための重要な調整です。
関連リンク
- Go Race Detector: https://go.dev/blog/race-detector
runtime.SetFinalizer
のドキュメント: https://pkg.go.dev/runtime#SetFinalizer
参考にした情報源リンク
- Goのガベージコレクションに関する一般的な情報源(公式ドキュメントやブログ記事など)
- Goのメモリ管理に関する技術記事
- GoのRace Detectorに関する詳細な解説
- Goのコミット履歴や関連するGo issueトラッカーの議論(特に「runtime: combine small NoScan allocations」に関するもの)
- Goのソースコード(特に
runtime
パッケージ) - Goのテストフレームワークに関する情報