[インデックス 16331] ファイルの概要
このコミットは、Go言語のmisc/cgo/test
ディレクトリにあるテストケースTestParallelSleep
の信頼性を向上させるための修正です。具体的には、テストが非決定的な結果(flakiness)を出す問題を解決し、並行して実行されるスリープ処理の開始時間の差をより正確に測定することで、テストの安定性を確保しています。
コミット
commit af1dd56d1bcb842a10adaa071cca3ad5bb687a38
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Sat May 18 02:55:44 2013 +0800
misc/cgo/test: deflake TestParallelSleep once more
Fixes #5480.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/9475043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/af1dd56d1bcb842a10adaa071cca3ad5bb687a38
元コミット内容
このコミットは、misc/cgo/test: deflake TestParallelSleep once more
というメッセージを持ち、Fixes #5480
と関連付けられています。これは、TestParallelSleep
というテストが以前から不安定であり、その問題を再度修正しようとする試みであることを示唆しています。
変更の背景
Go言語のテストスイートには、並行処理の正確性を検証するためのTestParallelSleep
というテストが存在しました。このテストは、GoルーチンとCgoを介したC言語のスレッドで同時にスリープ処理を実行し、それらが実際に並行して動作しているかを確認することを目的としていました。しかし、このテストはシステム負荷、スケジューリングの変動、または特定のアーキテクチャ(特にARM)でのタイミングの不正確さにより、頻繁に失敗する(flakyである)という問題に直面していました。
元のテストでは、2つのスリープ処理が完了するまでの合計時間を測定し、その時間が期待される並行実行の時間枠内に収まっているかを検証していました。しかし、このアプローチでは、システム全体の遅延やCPUの周波数調整(特にARMでのwasteCPU
関数の存在が示唆するように)といった外部要因に影響されやすく、真の並行性を正確に評価できていませんでした。
このコミットは、Go issue #5480で報告された問題を解決するために作成されました。この問題は、TestParallelSleep
が非決定的に失敗し、CI/CDパイプラインの安定性を損なっていたことを示しています。開発者は、スリープ処理の「開始時間」の差を測定することで、より堅牢な並行性チェックを実装し、テストの信頼性を向上させることを目指しました。
前提知識の解説
- Cgo: Go言語からC言語のコードを呼び出すためのメカニズムです。Goプログラム内でCの関数を直接利用したり、Cのライブラリとリンクしたりすることができます。このテストでは、GoルーチンからC言語の
sleep
関数を呼び出し、GoとCの並行実行を検証しています。 - Flaky Test (不安定なテスト): 実行するたびに成功したり失敗したりするテストのことです。コードのバグが原因ではなく、環境要因(タイミング、リソース競合、ネットワークの遅延など)やテスト自体の設計上の問題によって発生します。Flakyテストは開発者の生産性を低下させ、CI/CDパイプラインの信頼性を損ないます。
sleep
関数: プログラムの実行を指定された時間だけ一時停止させる関数です。C言語ではunistd.h
に含まれるsleep()
やwindows.h
に含まれるSleep()
などがあります。gettimeofday
/GetTickCount
: システムの現在時刻を取得するための関数です。gettimeofday
はUnix系システムでマイクロ秒単位の精度で時刻を取得し、GetTickCount
はWindowsシステムでミリ秒単位のシステム起動からの経過時間を取得します。これらは、処理の開始時刻を正確に記録するために使用されます。- 並行性 (Concurrency) と並列性 (Parallelism):
- 並行性: 複数のタスクが同時に進行しているように見える状態を指します。シングルコアCPUでも、タイムスライシングによって複数のタスクが切り替わりながら実行されることで実現されます。
- 並列性: 複数のタスクが物理的に同時に実行されている状態を指します。マルチコアCPUや分散システムで実現されます。
- このテストは、GoルーチンとCgoスレッドが「並行」に動作していることを確認しようとしていますが、理想的には「並列」に実行されることを期待しています。
技術的詳細
このコミットの主要な技術的変更点は、スリープ処理の並行性を検証する方法を、合計実行時間の測定から、並行して開始されたスリープ処理の「開始時間の差」の測定へと変更したことです。
mysleep
関数の導入:misc/cgo/test/callback_c.c
にmysleep
という新しいC関数が導入されました。- この関数は、
sleep
を呼び出す直前のシステム時刻(Unix系ではgettimeofday
、WindowsではGetTickCount
)を取得し、その時刻をミリ秒単位で返します。 - これにより、スリープ処理が実際に開始された正確な時刻を記録できるようになりました。
twoSleep
関数の変更:twoSleep
関数は、以前はBackgroundSleep
を呼び出した後に自身もsleep
を呼び出すだけでしたが、変更後はmysleep
を呼び出し、その開始時刻を返します。- これにより、Goルーチン側で開始されるスリープと、Cgoを介して開始されるスリープの、それぞれの開始時刻を比較するための基準点が提供されます。
parallelSleep
関数のロジック変更:- Go側の
parallelSleep
関数は、C.twoSleep
の戻り値(C側のスリープ開始時刻)と、sleepDone
チャネルから受け取る値(Go側のスリープ開始時刻)の差を計算するようになりました。 - この差が、2つのスリープ処理がどれだけ同時に開始されたかを示す指標となります。差が小さいほど、並行性が高いことを意味します。
- Go側の
sleepDone
チャネルの型変更:sleepDone
チャネルの型がbool
からint64
に変更されました。これは、スリープの完了を示すブール値ではなく、スリープの開始時刻(ミリ秒単位のint64
)をチャネルで送受信するためです。
- テストの検証ロジックの変更:
testParallelSleep
関数では、parallelSleep
が返す開始時間の差(dt
)が、sleepSec
(スリープ時間)の半分(time.Second/2
)を超えている場合にテストを失敗させるようになりました。- これは、もし2つのスリープが実質的に直列に実行された場合、開始時間の差がスリープ時間の約1倍になるため、その半分を超える差があれば並行性が損なわれていると判断できるというロジックに基づいています。
wasteCPU
関数の削除:- 以前のテストには、ARMアーキテクチャでのスリープの精度を向上させるためにCPUを浪費する
wasteCPU
関数が含まれていました。これは、電力管理がCPU周波数を上げることでスリープの精度が向上するという仮定に基づいていたようです。 - 新しいテストロジックは開始時間の差に焦点を当てているため、このような環境依存の調整が不要になり、削除されました。これにより、テストの複雑性が減り、よりポータブルになりました。
- 以前のテストには、ARMアーキテクチャでのスリープの精度を向上させるためにCPUを浪費する
これらの変更により、テストはシステム全体の実行時間ではなく、並行処理の「開始タイミングのずれ」という、より本質的な並行性の問題に焦点を当てるようになりました。これにより、外部要因によるテストの不安定性が大幅に軽減され、より信頼性の高いテスト結果が得られるようになりました。
コアとなるコードの変更箇所
misc/cgo/test/callback_c.c
--- a/misc/cgo/test/callback_c.c
+++ b/misc/cgo/test/callback_c.c
@@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.\n \n #include <sys/types.h>\n+#include <unistd.h>\n #include "_cgo_export.h"\n \n void\n@@ -29,9 +30,30 @@ IntoC(void)\n \tBackIntoGo();\n }\n \n-void\n+#ifdef WIN32
+#include <windows.h>
+long long
+mysleep(int seconds) {
+\tlong long st = GetTickCount();
+\tsleep(seconds);
+\treturn st;
+}
+#else
+#include <sys/time.h>
+long long
+mysleep(int seconds) {
+\tlong long st;
+\tstruct timeval tv;
+\tgettimeofday(&tv, NULL);
+\tst = tv.tv_sec * 1000 + tv.tv_usec / 1000;
+\tsleep(seconds);
+\treturn st;
+}
+#endif
+\n+long long
twoSleep(int n)\n {\n \tBackgroundSleep(n);\n-\tsleep(n);\n+\treturn mysleep(n);\n }\n```
### `misc/cgo/test/issue1560.go`
```diff
--- a/misc/cgo/test/issue1560.go
+++ b/misc/cgo/test/issue1560.go
@@ -5,71 +5,46 @@
package cgotest
/*
-#include <unistd.h>
+// mysleep returns the absolute start time in ms.
+long long mysleep(int seconds);
-unsigned int sleep(unsigned int seconds);
-\n-extern void BackgroundSleep(int);\n-void twoSleep(int);\n+// twoSleep returns the absolute start time of the first sleep
+// in ms.
+long long twoSleep(int);
*/
import "C"
import (
-\t"runtime"
\t"testing"
\t"time"
)
-var sleepDone = make(chan bool)
+var sleepDone = make(chan int64)
-func parallelSleep(n int) {
-\tC.twoSleep(C.int(n))
-\t<-sleepDone
+// parallelSleep returns the absolute difference between the start time
+// of the two sleeps.
+func parallelSleep(n int) int64 {
+\tt := int64(C.twoSleep(C.int(n))) - <-sleepDone
+\tif t < 0 {
+\t\treturn -t
+\t}
+\treturn t
}
//export BackgroundSleep
func BackgroundSleep(n int32) {
\tgo func() {
-\t\tC.sleep(C.uint(n))\n-\t\tsleepDone <- true
-\t}()
-\n-// wasteCPU starts a background goroutine to waste CPU
-// to cause the power management to raise the CPU frequency.
-// On ARM this has the side effect of making sleep more accurate.
-func wasteCPU() chan struct{} {
-\tdone := make(chan struct{})\n-\tgo func() {\n-\t\tfor {\n-\t\t\tselect {\n-\t\t\tcase <-done:\n-\t\t\t\treturn
-\t\t\tdefault:\n-\t\t\t}\n-\t\t}\n+\t\tsleepDone <- int64(C.mysleep(C.int(n)))
\t}()
-\t// pause for a short amount of time to allow the
-\t// power management to recognise load has risen.
-\t<-time.After(300 * time.Millisecond)
-\treturn done
}
func testParallelSleep(t *testing.T) {
-\tif runtime.GOARCH == "arm" {
-\t\t// on ARM, the 1.3s deadline is frequently missed,
-\t\t// and burning cpu seems to help
-\t\tdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(2))
-\t\tdefer close(wasteCPU())
-\t}
-\n \tsleepSec := 1
-\tstart := time.Now()
-\tparallelSleep(sleepSec)
-\tdt := time.Since(start)
-\tt.Logf("sleep(%d) slept for %v", sleepSec, dt)
+\tdt := time.Duration(parallelSleep(sleepSec)) * time.Millisecond
+\tt.Logf("difference in start time for two sleep(%d) is %v", sleepSec, dt)
\t// bug used to run sleeps in serial, producing a 2*sleepSec-second delay.
-\tif dt >= time.Duration(sleepSec)*1300*time.Millisecond {
+\t// we detect if the start times of those sleeps are > 0.5*sleepSec-second.
+\tif dt >= time.Duration(sleepSec)*time.Second/2 {
\t\tt.Fatalf("parallel %d-second sleeps slept for %f seconds", sleepSec, dt.Seconds())
\t}
}
コアとなるコードの解説
misc/cgo/test/callback_c.c
の変更
#include <unistd.h>
の追加:sleep
関数を使用するために必要です。mysleep
関数の追加:- この関数は、引数で指定された秒数だけスリープする前に、現在の時刻をミリ秒単位で取得し、その開始時刻を
long long
型で返します。 WIN32
マクロが定義されている場合はGetTickCount()
を使用し、それ以外の場合はgettimeofday()
を使用して時刻を取得します。これにより、OSに依存しない形で高精度な開始時刻の取得を試みています。
- この関数は、引数で指定された秒数だけスリープする前に、現在の時刻をミリ秒単位で取得し、その開始時刻を
twoSleep
関数の変更:- 以前は
BackgroundSleep(n)
を呼び出した後、自身もsleep(n)
を呼び出していました。 - 変更後は、
BackgroundSleep(n)
を呼び出した後、mysleep(n)
を呼び出し、その戻り値(スリープ開始時刻)をtwoSleep
の戻り値として返します。これにより、Go側でCgoを介して呼び出されたスリープの開始時刻をGo側で取得できるようになります。
- 以前は
misc/cgo/test/issue1560.go
の変更
- Cgoインポートブロックの変更:
#include <unistd.h>
の記述が削除され、代わりにmysleep
とtwoSleep
の関数プロトタイプ宣言が追加されました。これにより、GoコードからこれらのC関数を呼び出せるようになります。
sleepDone
チャネルの型変更:var sleepDone = make(chan bool)
からvar sleepDone = make(chan int64)
に変更されました。これは、スリープが完了したことを示すブール値ではなく、スリープの開始時刻(ミリ秒単位のint64
)をチャネルで送受信するためです。
parallelSleep
関数の変更:- 以前は単に
C.twoSleep(C.int(n))
を呼び出し、sleepDone
からの通知を待つだけでした。 - 変更後は、
C.twoSleep(C.int(n))
の戻り値(C側のスリープ開始時刻)から<-sleepDone
(Go側のスリープ開始時刻)を引くことで、2つのスリープの開始時間の差を計算します。 - 差が負の場合(Go側のスリープがC側より先に開始した場合)は絶対値を返します。これにより、常に正の差分が得られます。
- 以前は単に
BackgroundSleep
関数の変更:- 以前はGoルーチン内で
C.sleep(C.uint(n))
を呼び出し、完了後にsleepDone <- true
を送っていました。 - 変更後は、Goルーチン内で
C.mysleep(C.int(n))
を呼び出し、その戻り値(Go側のスリープ開始時刻)をsleepDone <- int64(...)
として送るようになりました。
- 以前はGoルーチン内で
wasteCPU
関数の削除:- この関数は、ARMアーキテクチャでのスリープ精度を向上させるためにCPUを意図的に消費するものでしたが、新しいテストロジックでは不要になったため削除されました。
testParallelSleep
関数の変更:runtime.GOARCH == "arm"
に関する条件分岐とwasteCPU
の呼び出しが削除されました。これにより、テストが特定のアーキテクチャに依存しなくなりました。dt
の計算が、parallelSleep
が返す開始時間の差をtime.Duration
に変換したものになりました。- テストの失敗条件が、
dt >= time.Duration(sleepSec)*1300*time.Millisecond
(合計実行時間が長すぎる場合)から、dt >= time.Duration(sleepSec)*time.Second/2
(開始時間の差がスリープ時間の半分を超える場合)に変更されました。これは、2つのスリープが並行ではなく直列に実行された場合に、開始時間の差が大きくなることを検出するためのものです。
これらの変更により、TestParallelSleep
は、スリープ処理の完了までの合計時間ではなく、GoルーチンとCgoスレッドで開始されるスリープ処理の「開始タイミングのずれ」を直接測定するようになりました。これにより、テストの信頼性が向上し、環境要因による不安定性が軽減されます。
関連リンク
- Go issue #5480: https://github.com/golang/go/issues/5480 (このコミットが修正した問題のトラッカー)
- Go CL 9475043: https://golang.org/cl/9475043 (このコミットに対応するGo Code Reviewの変更リスト)
参考にした情報源リンク
- Go issue #5480の議論内容
- Cgoの公式ドキュメント
sleep
、gettimeofday
、GetTickCount
などのシステムコールに関する一般的なドキュメント- Go言語のチャネルと並行性に関する一般的な知識
- テストのflakinessに関する一般的なソフトウェアエンジニアリングの概念