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

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

このコミットは、Go言語の実験的なinotifyパッケージにおけるテスト中のデータ競合(data race)を防止するための修正です。具体的には、inotify_linux_test.goファイル内でイベント受信数をカウントする際に発生していた競合状態を、sync/atomicパッケージの関数を使用することで安全に修正しています。

コミット

commit f5f3c3fe093fc359045a3818d3cd04f7b40b06c2
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Sun Jun 24 19:22:48 2012 -0400

    exp/inotify: prevent data race during testing
    Fixes #3714.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6341047

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

https://github.com/golang/go/commit/f5f3c3fe093fc359045a3818d3cd04f7b40b06c2

元コミット内容

exp/inotify: prevent data race during testing
Fixes #3714.

変更の背景

このコミットの背景には、Go言語のexp/inotifyパッケージのテストコードにおいて、並行処理中に発生するデータ競合の問題がありました。inotifyはLinuxカーネルのファイルシステムイベント監視機能を提供するもので、このパッケージはそのGo言語バインディングです。テストコードでは、ファイルシステムイベントを監視し、発生したイベントの数をカウントしていました。

問題は、イベントを非同期で受信するゴルーチン(goroutine)と、そのイベント数を参照するメインのテストゴルーチンとの間で、共有変数eventsReceivedへのアクセスが同期されていなかった点にあります。複数のゴルーチンが同時に同じ変数に対して読み書きを行うと、予期せぬ結果やテストの不安定性(テストが時々失敗する、または誤った結果を報告する)を引き起こすデータ競合が発生します。

コミットメッセージにあるFixes #3714.は、このデータ競合がGoのIssueトラッカーで報告された問題(Issue 3714)を修正するものであることを示しています。データ競合はデバッグが困難なバグの一つであり、テストの信頼性を確保するためには、このような並行処理の問題を適切に解決する必要があります。

前提知識の解説

1. データ競合 (Data Race)

データ競合は、複数の並行実行されるスレッド(Goではゴルーチン)が、少なくとも1つが書き込み操作である共有メモリ上の同じ変数に同時にアクセスする際に発生する競合状態の一種です。データ競合が発生すると、プログラムの動作が非決定論的になり、予期せぬ結果、クラッシュ、またはセキュリティ脆弱性につながる可能性があります。Go言語では、データ競合を検出するためのツール(go run -race)が提供されています。

2. sync/atomicパッケージ

Go言語の標準ライブラリであるsync/atomicパッケージは、低レベルのアトミック操作を提供します。アトミック操作とは、不可分(分割不可能)な操作のことで、その操作が完了するまで他のゴルーチンから割り込まれることがありません。これにより、複数のゴルーチンが共有変数にアクセスする際にデータ競合を防ぎ、安全な並行処理を実現できます。

主なアトミック操作には以下のようなものがあります。

  • AddInt32, AddInt64: 整数値の加算をアトミックに行う。
  • LoadInt32, LoadInt64, LoadPointerなど: 変数の値をアトミックに読み込む。
  • StoreInt32, StoreInt64, StorePointerなど: 変数に値をアトミックに書き込む。
  • CompareAndSwapInt32, CompareAndSwapInt64など: 特定の値であれば新しい値に交換する(CAS操作)。

これらの関数は、ミューテックス(sync.Mutex)のようなロック機構よりも粒度が細かく、特定の単純な操作においてはより効率的です。

3. inotify (Linux)

inotifyはLinuxカーネルが提供するファイルシステムイベント監視メカニズムです。ファイルやディレクトリに対する変更(作成、削除、移動、アクセス、変更など)を監視し、イベントが発生した際にアプリケーションに通知します。Goのexp/inotifyパッケージは、このinotify機能へのGo言語からのインターフェースを提供します。

4. Goの並行処理 (Goroutines and Channels)

Go言語は、軽量なスレッドである「ゴルーチン(goroutine)」と、ゴルーチン間の安全な通信を可能にする「チャネル(channel)」を言語レベルでサポートすることで、並行処理を容易にしています。

  • ゴルーチン: goキーワードを使って関数を呼び出すことで、新しいゴルーチンが生成され、その関数が並行して実行されます。
  • チャネル: ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、データ競合を避けるための安全な方法として推奨されます("Don't communicate by sharing memory; share memory by communicating.")。

今回のケースでは、チャネルはイベントのストリームを受信するために使用されていますが、イベント数をカウントする変数自体はチャネルを介して共有されていなかったため、データ競合が発生していました。

技術的詳細

このコミットの技術的な核心は、共有変数eventsReceivedへのアクセスを、通常の整数操作からsync/atomicパッケージのアトミック操作に置き換えることで、データ競合を解消した点にあります。

元のコードでは、eventsReceivedは単なるint型の変数として宣言され、eventsReceived++という操作でインクリメントされていました。この++操作は、実際には以下の3つのステップから構成されます。

  1. eventsReceivedの現在の値をメモリから読み込む。
  2. 読み込んだ値に1を加算する。
  3. 新しい値をeventsReceivedにメモリに書き込む。

複数のゴルーチンが同時にこの操作を実行しようとすると、例えばゴルーチンAが値を読み込んだ直後にゴルーチンBが値を読み込み、それぞれが加算して書き込むと、最終的な値が期待よりも小さくなる可能性があります(更新が失われる)。

修正後のコードでは、以下の変更が行われました。

  1. eventsReceivedの型変更: var eventsReceived = 0からvar eventsReceived int32 = 0に変更されました。sync/atomicパッケージの関数は、int32int64のような特定のサイズの整数型を引数に取ります。
  2. インクリメント操作の変更: eventsReceived++atomic.AddInt32(&eventsReceived, 1)に置き換えられました。atomic.AddInt32は、指定されたint32型変数(ポインタで渡す)に指定された値をアトミックに加算し、その結果を返します。この操作は不可分であるため、複数のゴルーチンが同時に呼び出しても、すべての加算が正しく反映され、更新が失われることはありません。
  3. 値の読み込み操作の変更: if eventsReceived == 0if atomic.LoadInt32(&eventsReceived) == 0に置き換えられました。atomic.LoadInt32は、指定されたint32型変数(ポインタで渡す)の値をアトミックに読み込みます。これにより、読み込み操作自体も他の書き込み操作と競合することなく、常に最新の正確な値を取得できます。

これらの変更により、eventsReceived変数へのすべてのアクセス(読み込みと書き込み)がアトミック操作によって保護され、テスト中のデータ競合が完全に解消されました。

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

--- a/src/pkg/exp/inotify/inotify_linux_test.go
+++ b/src/pkg/exp/inotify/inotify_linux_test.go
@@ -9,6 +9,7 @@ package inotify
 import (
 	"io/ioutil"
 	"os"
+	"sync/atomic"
 	"testing"
 	"time"
 )
@@ -43,13 +44,13 @@ func TestInotifyEvents(t *testing.T) {
 
 	// Receive events on the event channel on a separate goroutine
 	eventstream := watcher.Event
-	var eventsReceived = 0
+	var eventsReceived int32 = 0
 	done := make(chan bool)
 	go func() {
 		for event := range eventstream {
 			// Only count relevant events
 			if event.Name == testFile {
-				eventsReceived++
+				atomic.AddInt32(&eventsReceived, 1)
 				t.Logf("event received: %s", event)
 			} else {
 				t.Logf("unexpected event received: %s", event)
@@ -67,7 +68,7 @@ func TestInotifyEvents(t *testing.T) {
 
 	// We expect this event to be received almost immediately, but let's wait 1 s to be sure
 	time.Sleep(1 * time.Second)
-	if eventsReceived == 0 {
+	if atomic.LoadInt32(&eventsReceived) == 0 {
 		t.Fatal("inotify event hasn't been received after 1 second")
 	}
 

コアとなるコードの解説

1. import "sync/atomic" の追加

+	"sync/atomic"

sync/atomicパッケージの関数を使用するために、インポート宣言に追加されています。

2. eventsReceived の型変更

-	var eventsReceived = 0
+	var eventsReceived int32 = 0

eventsReceived変数の型が、通常のint(またはデフォルトのint)からint32に明示的に変更されました。これは、sync/atomicパッケージの関数がint32int64などの特定のサイズの整数型を操作するためです。

3. イベントカウントのインクリメント

-				eventsReceived++
+				atomic.AddInt32(&eventsReceived, 1)

イベントが受信された際にeventsReceivedをインクリメントする部分が変更されました。

  • 変更前: eventsReceived++ は、非アトミックな読み込み、加算、書き込みの3ステップからなる操作であり、複数のゴルーチンから同時にアクセスされるとデータ競合が発生する可能性がありました。
  • 変更後: atomic.AddInt32(&eventsReceived, 1) は、eventsReceived変数のアドレスを渡し、それに1をアトミックに加算します。この操作は不可分であり、他のゴルーチンからの干渉を受けずに安全に実行されるため、データ競合が防止されます。

4. イベントカウントのチェック

-	if eventsReceived == 0 {
+	if atomic.LoadInt32(&eventsReceived) == 0 {

テストの最後にeventsReceivedの値をチェックする部分が変更されました。

  • 変更前: eventsReceived == 0 は、非アトミックな読み込み操作であり、eventsReceivedが別のゴルーチンによって同時に更新されている場合、古い値や部分的に更新された値を読み取ってしまう可能性がありました。
  • 変更後: atomic.LoadInt32(&eventsReceived) は、eventsReceived変数のアドレスを渡し、その値をアトミックに読み込みます。これにより、常に最新かつ正確な値が取得され、テストの信頼性が向上します。

これらの変更により、eventsReceived変数へのすべてのアクセスがアトミック操作によって保護され、テスト中のデータ競合が解消されました。

関連リンク

参考にした情報源リンク