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

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

このコミットは、Goランタイムのロックフリースタックのテスト (TestLFStackStress) におけるバグを修正するものです。具体的には、テスト中にスタックにプッシュされたノードがガベージコレクションによって早期に回収されてしまう問題を解決し、テストの信頼性を向上させています。

コミット

commit 98178b345ac7c4fb711e3327b16b1230cdab9d25
Author: Russ Cox <rsc@golang.org>
Date:   Fri Jan 17 17:42:24 2014 -0500

    runtime: fix TestLFStackStress
    
    Fixes #7138.
    
    R=r, bradfitz, dave
    CC=dvyukov, golang-codereviews
    https://golang.org/cl/53910043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/98178b345ac7c4fb711e3327b16b1230cdab9d25

元コミット内容

runtime: fix TestLFStackStress

このコミットは、GoランタイムのTestLFStackStressテストを修正するものです。Issue #7138を修正します。

変更の背景

このコミットの背景には、GoランタイムのロックフリースタックのテストであるTestLFStackStressが、特定の条件下で不安定になる問題がありました。具体的には、テスト中にスタックにプッシュされたMyNodeオブジェクトが、Goのガベージコレクタによって意図せず早期に回収されてしまうことが原因でした。

ロックフリーデータ構造のテストは、並行処理の複雑さから非常にデリケートです。特に、ガベージコレクタが介入する環境では、オブジェクトのライフタイム管理が重要になります。TestLFStackStressは、複数のゴルーチンが同時にロックフリースタックに対してプッシュ・ポップ操作を行うことで、その堅牢性を検証することを目的としていました。しかし、スタックにプッシュされたノードへの参照が、テストコード内で適切に保持されていなかったため、ガベージコレクタがそれらのノードを「到達不可能」と判断し、テストが完了する前に回収してしまっていたのです。これにより、テストが失敗したり、予期せぬ動作を引き起こしたりする可能性がありました。

Issue #7138は、この不安定なテストの挙動を報告しており、このコミットはその問題を解決するために作成されました。

前提知識の解説

ロックフリーデータ構造 (Lock-Free Data Structures)

ロックフリーデータ構造は、ミューテックスやセマフォなどのロック機構を使用せずに、複数のスレッド(Goにおいてはゴルーチン)が同時にアクセスしても正しく動作するように設計されたデータ構造です。これにより、ロックの競合によるパフォーマンスの低下やデッドロックのリスクを回避できます。

ロックフリーデータ構造の実装には、通常、アトミック操作(不可分操作)が用いられます。これは、複数のCPU命令からなる一連の操作が、他のスレッドから見て中断されることなく、単一の不可分な操作として実行されることを保証するものです。Go言語では、sync/atomicパッケージがアトミック操作を提供しています。

ロックフリースタックは、その一例であり、通常はCAS (Compare-And-Swap) などのアトミック操作を用いて、スタックの先頭(ヘッド)ポインタを更新することでプッシュ・ポップを実現します。

ガベージコレクション (Garbage Collection, GC)

ガベージコレクションは、プログラムが動的に確保したメモリ領域のうち、もはやプログラムから到達不可能になった(参照されなくなった)ものを自動的に解放する仕組みです。Go言語はトレース型ガベージコレクタを採用しており、プログラムが実行中に参照しているオブジェクトを追跡し、参照されていないオブジェクトを「ガベージ」と判断して回収します。

ガベージコレクタは、メモリリークを防ぎ、開発者が手動でメモリを管理する手間を省くという利点がありますが、その動作はプログラムのパフォーマンスに影響を与える可能性があります。特に、オブジェクトのライフタイムが短く、頻繁に生成・破棄されるようなシナリオでは、GCのオーバーヘッドが顕著になることがあります。

Goのポインタと参照

Go言語では、変数は値渡しが基本ですが、ポインタを使用することで間接的に値を参照できます。ガベージコレクタは、プログラム内のポインタを追跡し、どのオブジェクトがまだ参照されているかを判断します。もし、あるオブジェクトへのすべての参照が失われた場合、そのオブジェクトはガベージコレクタの対象となります。

今回の問題では、MyNodeオブジェクトがロックフリースタックにプッシュされた後、テストコード内でそのMyNodeオブジェクトへの直接的な参照が失われていたため、ガベージコレクタが誤ってそれらを回収してしまっていたのです。ロックフリースタック自体は、uint64としてエンコードされたポインタを保持していましたが、Goの型システムはそれを直接的な参照とは見なしません。

技術的詳細

このコミットの技術的な核心は、Goのガベージコレクタがオブジェクトの到達可能性をどのように判断するか、そしてロックフリーデータ構造のテストにおいて、その挙動をどのように考慮すべきかという点にあります。

元のTestLFStackStressでは、MyNode構造体のインスタンスを生成し、そのポインタをロックフリースタックにプッシュしていました。しかし、プッシュ後、これらのMyNodeインスタンスへの参照は、nodesというローカルスライスに一時的に追加されるだけでした。

// 変更前
var nodes []*MyNode // ローカル変数
// ...
nodes = append(nodes, node)
LFStackPush(stacks[i%2], fromMyNode(node))

このnodesスライスは、TestLFStackStress関数のスコープ内で定義されており、関数が終了するとその参照は失われます。さらに重要なのは、LFStackPush関数は*MyNodeを直接受け取るのではなく、uint64に変換されたポインタ値を受け取ります。Goのガベージコレクタは、このuint64の値を直接的なポインタ参照として追跡しません。そのため、nodesスライスがテストの途中でガベージコレクションの対象となり、その中のMyNodeインスタンスへの参照が失われると、スタックにプッシュされたMyNodeインスタンスもガベージコレクタによって回収されてしまう可能性がありました。

この問題は、特に並行処理が行われるTestLFStackStressのようなテストで顕著になります。複数のゴルーチンが同時にスタックを操作している最中に、ガベージコレクタが介入し、まだスタック上に存在するはずのノードを回収してしまうと、テストは不正な状態になり、失敗する原因となります。

このコミットでは、この問題を解決するために、nodesスライスをローカル変数からパッケージレベルのグローバル変数stressに変更しました。

// 変更後
var stress []*MyNode // パッケージレベルのグローバル変数
// ...
stress = nil // テスト開始時に初期化
// ...
stress = append(stress, node) // グローバル変数に追加
// ...
// テスト終了後
stress = nil // ガベージコレクションを許可するためにnilに設定

stressがパッケージレベルのグローバル変数になったことで、TestLFStackStress関数が終了しても、その参照は失われません。これにより、テストの実行中にガベージコレクタが介入しても、stressスライスがMyNodeインスタンスへの参照を保持し続けるため、それらのインスタンスが早期に回収されることを防ぎます。テストが完全に終了し、すべての検証が完了した後に、stress = nilと明示的に設定することで、MyNodeインスタンスがガベージコレクションの対象となることを許可しています。

この変更は、テストの信頼性を高め、ロックフリースタックの実装が正しく機能していることをより確実に検証できるようにするために不可欠でした。

コアとなるコードの変更箇所

変更はsrc/pkg/runtime/lfstack_test.goファイルに集中しています。

--- a/src/pkg/runtime/lfstack_test.go
+++ b/src/pkg/runtime/lfstack_test.go
@@ -71,6 +71,8 @@ func TestLFStack(t *testing.T) {
 	}
 }
 
+var stress []*MyNode
+
 func TestLFStackStress(t *t.T) {
 	const K = 100
 	P := 4 * GOMAXPROCS(-1)
@@ -80,14 +82,15 @@ func TestLFStackStress(t *t.T) {
 	}
 	// Create 2 stacks.
 	stacks := [2]*uint64{new(uint64), new(uint64)}
-	// Need to keep additional referenfces to nodes, the stack is not all that type-safe.
-	var nodes []*MyNode
-	// Push K elements randomly onto the stacks.
+	// Need to keep additional references to nodes,
+	// the lock-free stack is not type-safe.
+	stress = nil
 	sum := 0
 	for i := 0; i < K; i++ {
 		sum += i
 		node := &MyNode{data: i}
-		nodes = append(nodes, node)
+		stress = append(stress, node)
 		LFStackPush(stacks[i%2], fromMyNode(node))
 	}
 	c := make(chan bool, P)
@@ -127,4 +130,7 @@ func TestLFStackStress(t *t.T) {
 	if sum2 != sum {
 		t.Fatalf("Wrong sum %d/%d", sum2, sum)
 	}
+
+	// Let nodes be collected now.
+	stress = nil
 }

コアとなるコードの解説

  1. var stress []*MyNode の追加: TestLFStackStress関数の外、つまりパッケージレベルでstressという*MyNode型のスライスが宣言されました。これにより、stressはグローバルな参照として機能し、TestLFStackStress関数が終了しても、その中の要素(MyNodeインスタンス)への参照がガベージコレクタから見えるようになります。

  2. stress = nil の初期化: TestLFStackStress関数の開始時にstress = nilが追加されました。これは、テストが複数回実行される場合に、前回のテストで残ったMyNodeインスタンスへの参照をクリアし、テストの独立性を保つためです。

  3. nodes から stress への変更: 元のコードではvar nodes []*MyNodeというローカルスライスが使われていましたが、これが削除され、代わりにstress = append(stress, node)として、パッケージレベルのstressスライスにMyNodeインスタンスが追加されるようになりました。これにより、MyNodeインスタンスはテストの実行中ずっと参照され続けることが保証されます。

  4. テスト終了後の stress = nil: TestLFStackStress関数の最後にstress = nilが追加されました。これは、テストが正常に完了し、MyNodeインスタンスがもはや必要なくなった時点で、それらへの参照を明示的に解除し、ガベージコレクタがそれらを回収できるようにするためです。これにより、メモリリークを防ぎ、テスト後のクリーンアップを適切に行います。

これらの変更により、TestLFStackStressは、ロックフリースタックにプッシュされたMyNodeインスタンスが、テストの完了前にガベージコレクタによって誤って回収されることを防ぎ、テストの信頼性と安定性を大幅に向上させました。

関連リンク

参考にした情報源リンク