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

[インデックス 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スレッドが「並行」に動作していることを確認しようとしていますが、理想的には「並列」に実行されることを期待しています。

技術的詳細

このコミットの主要な技術的変更点は、スリープ処理の並行性を検証する方法を、合計実行時間の測定から、並行して開始されたスリープ処理の「開始時間の差」の測定へと変更したことです。

  1. mysleep関数の導入:
    • misc/cgo/test/callback_c.cmysleepという新しいC関数が導入されました。
    • この関数は、sleepを呼び出す直前のシステム時刻(Unix系ではgettimeofday、WindowsではGetTickCount)を取得し、その時刻をミリ秒単位で返します。
    • これにより、スリープ処理が実際に開始された正確な時刻を記録できるようになりました。
  2. twoSleep関数の変更:
    • twoSleep関数は、以前はBackgroundSleepを呼び出した後に自身もsleepを呼び出すだけでしたが、変更後はmysleepを呼び出し、その開始時刻を返します。
    • これにより、Goルーチン側で開始されるスリープと、Cgoを介して開始されるスリープの、それぞれの開始時刻を比較するための基準点が提供されます。
  3. parallelSleep関数のロジック変更:
    • Go側のparallelSleep関数は、C.twoSleepの戻り値(C側のスリープ開始時刻)と、sleepDoneチャネルから受け取る値(Go側のスリープ開始時刻)の差を計算するようになりました。
    • この差が、2つのスリープ処理がどれだけ同時に開始されたかを示す指標となります。差が小さいほど、並行性が高いことを意味します。
  4. sleepDoneチャネルの型変更:
    • sleepDoneチャネルの型がboolからint64に変更されました。これは、スリープの完了を示すブール値ではなく、スリープの開始時刻(ミリ秒単位のint64)をチャネルで送受信するためです。
  5. テストの検証ロジックの変更:
    • testParallelSleep関数では、parallelSleepが返す開始時間の差(dt)が、sleepSec(スリープ時間)の半分(time.Second/2)を超えている場合にテストを失敗させるようになりました。
    • これは、もし2つのスリープが実質的に直列に実行された場合、開始時間の差がスリープ時間の約1倍になるため、その半分を超える差があれば並行性が損なわれていると判断できるというロジックに基づいています。
  6. wasteCPU関数の削除:
    • 以前のテストには、ARMアーキテクチャでのスリープの精度を向上させるためにCPUを浪費するwasteCPU関数が含まれていました。これは、電力管理が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>の記述が削除され、代わりにmysleeptwoSleepの関数プロトタイプ宣言が追加されました。これにより、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(...)として送るようになりました。
  • 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の議論内容
  • Cgoの公式ドキュメント
  • sleepgettimeofdayGetTickCountなどのシステムコールに関する一般的なドキュメント
  • Go言語のチャネルと並行性に関する一般的な知識
  • テストのflakinessに関する一般的なソフトウェアエンジニアリングの概念