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

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

このコミットは、Goランタイムにおけるレース検出器(Race Detector)とシステムコール(Syscall)ベンチマークの間の競合状態(race condition)を修正するものです。具体的には、レース検出器が有効な環境下でシステムコール関連のベンチマークを実行すると発生する問題を回避するために、これらのベンチマークをレース検出器の対象外とする変更が行われました。

コミット

commit 5c8ad2e13dc4fd69d116562876d67b87896e963c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Jul 30 22:13:51 2013 +0400

    runtime: fix race builders
    Do not run Syscall benchmarks under race detector,
    they split stack in syscall status.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/12093045

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

https://github.com/golang/go/commit/5c8ad2e13dc4fd69d116562876d67b87896e963c

元コミット内容

このコミットの目的は「runtime: fix race builders」であり、レース検出器が有効なビルド環境("race builders")で発生する問題を修正することです。具体的には、システムコール関連のベンチマークがレース検出器の下で実行されないように変更されました。その理由は、これらのベンチマークがシステムコール中にスタックを分割("split stack in syscall status")するため、レース検出器と競合が発生する可能性があるためです。

変更の背景

Go言語のランタイムは、並行処理を効率的に扱うためにゴルーチン(goroutine)とスケジューラを内蔵しています。システムコールは、ユーザー空間のプログラムがOSカーネルの機能(ファイルI/O、ネットワーク通信など)を利用するためのインターフェースです。Goランタイムは、ゴルーチンがシステムコールを実行する際に、そのゴルーチンのスタックを一時的に切り離したり(スタックの分割)、OSスレッドにアタッチしたりするなどの処理を行います。

一方、Goにはデータ競合(data race)を検出するための強力なツールであるレース検出器が組み込まれています。これは、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生する競合状態を特定するのに役立ちます。レース検出器は、プログラムの実行中にメモリアクセスを監視し、潜在的な競合を報告します。

このコミットが行われた当時、システムコール関連のベンチマーク(BenchmarkSyscallなど)がレース検出器が有効な環境で実行されると、誤ったデータ競合が報告される問題が発生していました。これは、システムコール中のスタックの分割や、ランタイム内部での低レベルなメモリ操作が、レース検出器の監視メカニズムと予期せぬ相互作用を起こしたためと考えられます。レース検出器は、本来アプリケーションコードのデータ競合を検出するためのものであり、ランタイム内部の低レベルな処理、特にシステムコールのようなOSとの境界での処理は、その監視対象から除外されるべき、あるいは特別な扱いが必要でした。

この問題は、GoのCI/CDシステム("race builders")でベンチマークが実行される際に、ビルドが失敗したり、誤った警告が大量に発生したりする原因となっていました。そのため、これらのベンチマークをレース検出器の対象外とすることで、CIの安定性を確保し、レース検出器が本来検出すべきアプリケーションレベルのデータ競合に集中できるようにすることが目的でした。

前提知識の解説

Goのレース検出器 (Race Detector)

Goのレース検出器は、Go 1.1で導入された強力なデバッグツールです。go run -racego build -racego test -raceなどのコマンドで有効にできます。有効にすると、プログラムの実行中にメモリアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、そのうち少なくとも1つが書き込み操作である場合に、データ競合として報告します。データ競合は、並行プログラムにおける最も一般的なバグの原因の一つであり、予測不能な動作やクラッシュを引き起こす可能性があります。レース検出器は、これらのバグを開発段階で早期に発見するのに非常に有効です。

システムコール (Syscall)

システムコールは、オペレーティングシステム(OS)のカーネルが提供するサービスを、ユーザー空間のプログラムが利用するためのメカニズムです。ファイルI/O(ファイルの読み書き)、ネットワーク通信(ソケットの作成とデータの送受信)、プロセス管理(新しいプロセスの作成)、メモリ管理など、OSの機能にアクセスする際にシステムコールが使用されます。Goプログラムがシステムコールを実行する際には、GoランタイムがOSと連携し、ゴルーチンをOSスレッドにマッピングしたり、スタックを切り替えたりするなどの低レベルな処理が行われます。

スタックの分割 (Stack Splitting)

Goのゴルーチンは、非常に軽量なスレッドのようなもので、OSスレッドよりもはるかに少ないメモリで開始されます(通常は数KB)。ゴルーチンが関数呼び出しを深くネストしたり、大きなローカル変数を確保したりしてスタックが不足しそうになると、Goランタイムは自動的にスタックを拡張します。このプロセスは「スタックの分割(Stack Splitting)」と呼ばれ、既存のスタックの隣に新しい、より大きなスタックセグメントを割り当て、古いスタックの内容を新しいスタックにコピーすることで行われます。これにより、ゴルーチンは必要に応じてスタックサイズを動的に調整でき、メモリ効率が向上します。

システムコール中には、ゴルーチンはユーザー空間からカーネル空間に移行します。この際、GoランタイムはゴルーチンのスタックをOSスレッドのスタックに切り替えるなどの処理を行います。このスタックの切り替えや、システムコールから戻ってきた際のスタックの再構築の過程で、レース検出器が誤ってデータ競合を検出する可能性がありました。これは、レース検出器が監視しているメモリ領域が、ランタイムの内部的なスタック管理操作によって一時的に不整合な状態に見えるためです。

技術的詳細

このコミットの技術的な解決策は、Goのビルドタグ(build tag)を利用して、特定のテストファイルがレース検出器が有効なビルドではコンパイルされないようにすることです。

Goのビルドタグは、ソースファイルの先頭に// +build tagnameのようなコメントを追加することで、そのファイルが特定のビルド条件でのみコンパイルされるように指定するメカニズムです。

このコミットでは、以下の変更が行われました。

  1. src/pkg/runtime/norace_test.go の新規作成:

    • このファイルには、BenchmarkSyscallおよび関連するベンチマーク関数(BenchmarkSyscallWork, BenchmarkSyscallExcess, BenchmarkSyscallExcessWork, benchmarkSyscall)が移動されました。
    • このファイルの先頭には、// +build !race というビルドタグが追加されました。
    • このタグは、「raceタグがない場合にのみこのファイルをコンパイルする」という意味です。つまり、go test -raceのようにレース検出器を有効にしてビルドする際には、このファイルはコンパイルされず、したがってこのファイル内のベンチマークは実行されません。
  2. src/pkg/runtime/proc_test.go からのベンチマーク関数の削除:

    • 既存のproc_test.goファイルから、BenchmarkSyscallおよび関連するベンチマーク関数が削除されました。これにより、proc_test.goはレース検出器が有効な環境でも問題なくコンパイル・実行されるようになります。

この変更により、システムコール関連のベンチマークは、レース検出器が有効な環境では実行されなくなり、誤ったデータ競合の報告が回避されるようになりました。これは、ランタイムの内部的なスタック管理とレース検出器の相互作用による問題を、テストの実行環境を分離することで解決する、実用的なアプローチです。

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

src/pkg/runtime/norace_test.go (新規作成)

// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// The file contains tests that can not run under race detector for some reason.
// +build !race

package runtime_test

import (
	"runtime"
	"sync/atomic"
	"testing"
)

// Syscall tests split stack between Entersyscall and Exitsyscall under race detector.
func BenchmarkSyscall(b *testing.B) {
	benchmarkSyscall(b, 0, 1)
}

func BenchmarkSyscallWork(b *testing.B) {
	benchmarkSyscall(b, 100, 1)
}

func BenchmarkSyscallExcess(b *testing.B) {
	benchmarkSyscall(b, 0, 4)
}

func BenchmarkSyscallExcessWork(b *testing.B) {
	benchmarkSyscall(b, 100, 4)
}

func benchmarkSyscall(b *testing.B, work, excess int) {
	const CallsPerSched = 1000
	procs := runtime.GOMAXPROCS(-1) * excess
	N := int32(b.N / CallsPerSched)
	c := make(chan bool, procs)
	for p := 0; p < procs; p++ {
		go func() {
			foo := 42
			for atomic.AddInt32(&N, -1) >= 0 {
				runtime.Gosched()
				for g := 0; g < CallsPerSched; g++ {
					runtime.Entersyscall()
					for i := 0; i < work; i++ {
						foo *= 2
						foo /= 2
					}
					runtime.Exitsyscall()
				}
			}
			c <- foo == 42
		}()
	}
	for p := 0; p < procs; p++ {
		<-c
	}
}

src/pkg/runtime/proc_test.go (変更箇所 - 削除)

--- a/src/pkg/runtime/proc_test.go
+++ b/src/pkg/runtime/proc_test.go
@@ -344,49 +344,6 @@ func BenchmarkStackGrowthDeep(b *testing.B) {
 	benchmarkStackGrowth(b, 1024)
 }
 
-func BenchmarkSyscall(b *testing.B) {
-	benchmarkSyscall(b, 0, 1)
-}
-
-func BenchmarkSyscallWork(b *testing.B) {
-	benchmarkSyscall(b, 100, 1)
-}
-
-func BenchmarkSyscallExcess(b *testing.B) {
-	benchmarkSyscall(b, 0, 4)
-}
-
-func BenchmarkSyscallExcessWork(b *testing.B) {
-	benchmarkSyscall(b, 100, 4)
-}
-
-func benchmarkSyscall(b *testing.B, work, excess int) {
-	const CallsPerSched = 1000
-	procs := runtime.GOMAXPROCS(-1) * excess
-	N := int32(b.N / CallsPerSched)
-	c := make(chan bool, procs)
-	for p := 0; p < procs; p++ {
-		go func() {
-			foo := 42
-			for atomic.AddInt32(&N, -1) >= 0 {
-				runtime.Gosched()
-				for g := 0; g < CallsPerSched; g++ {
-					runtime.Entersyscall()
-					for i := 0; i < work; i++ {
-						foo *= 2
-						foo /= 2
-					}
-					runtime.Exitsyscall()
-				}
-			}
-			c <- foo == 42
-		}()
-	}
-	for p := 0; p < procs; p++ {
-		<-c
-	}
-}
-
 func BenchmarkCreateGoroutines(b *testing.B) {
 	benchmarkCreateGoroutines(b, 1)
 }

コアとなるコードの解説

norace_test.go の役割

新しく作成された norace_test.go ファイルは、そのファイル名が示す通り、「レース検出器なし」の環境で実行されるテストを格納するためのものです。

  • // +build !race: この行がこのファイルの最も重要な部分です。これはGoのビルドタグであり、Goコンパイラに対して、raceというビルドタグが指定されていない場合にのみこのファイルをコンパイルするように指示します。つまり、go test -raceのようにレース検出器を有効にしてテストを実行しようとすると、このファイルはコンパイル対象から外され、その中のベンチマーク関数は実行されません。
  • package runtime_test: runtimeパッケージのテストであることを示します。
  • import (...): 必要なパッケージ(runtime, sync/atomic, testing)をインポートしています。
  • BenchmarkSyscall などのベンチマーク関数: これらの関数は、システムコールがGoランタイムのパフォーマンスに与える影響を測定するためのベンチマークです。
    • runtime.Entersyscall()runtime.Exitsyscall(): これらはGoランタイムの内部関数で、ゴルーチンがシステムコールに入る前と出た後に呼び出されます。これらの関数は、スケジューラがゴルーチンの状態を適切に管理し、OSスレッドとの連携を行うために使用されます。
    • runtime.Gosched(): 現在のゴルーチンを一時停止し、他のゴルーチンにCPUを譲るようにスケジューラにヒントを与えます。これにより、並行性がシミュレートされます。
    • atomic.AddInt32(&N, -1): 複数のゴルーチンが共有するカウンタをアトミックに減算しています。これは、ベンチマークの実行回数を安全に管理するためです。
    • foo *= 2; foo /= 2;: workパラメータが指定された場合に、システムコール中にダミーの計算を行うことで、システムコール中のCPU使用率をシミュレートしています。

proc_test.go からの削除

proc_test.go から BenchmarkSyscall および関連するベンチマーク関数が削除されたことで、このファイルはレース検出器が有効な環境でも問題なくコンパイル・実行されるようになりました。これにより、GoのCIシステムにおける「race builders」での誤検出が解消され、CIの安定性が向上しました。

この変更は、Goランタイムの内部的な複雑さと、レース検出器のような強力なツールが低レベルなランタイムの挙動とどのように相互作用するかを示す良い例です。ランタイムの特定の側面(この場合はシステムコール中のスタック管理)がレース検出器の監視モデルと完全に一致しない場合、ビルドタグのようなメカニズムを使用して、特定のテストを特定の環境から除外することが、実用的な解決策となります。

関連リンク

参考にした情報源リンク

  • Goの公式ドキュメント
  • Goのソースコード
  • GoのIssueトラッカー (CL 12093045)
  • データ競合とレース検出器に関する一般的なプログラミングの概念