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

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

このコミットは、Go言語の標準ライブラリであるtimeパッケージにおける競合状態(race condition)を修正するものです。具体的には、タイムゾーン情報の初期化に関連する問題に対処し、テストの堅牢性を向上させるための変更が含まれています。

コミット

commit fd1abac71c1792527943696c1c84bbbe7dac2ac3
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Jan 14 14:09:42 2013 -0800

    time: fix race
    
    Fixes #4622
    
    R=golang-dev, dave, dvyukov
    CC=golang-dev
    https://golang.org/cl/7103046

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

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

元コミット内容

このコミットは、timeパッケージ内の競合状態を修正します。具体的には、Go Issue #4622で報告された問題に対応しています。

変更されたファイルは以下の通りです。

  • src/pkg/time/export_test.go: 新規ファイル。テスト目的で内部状態をリセットする関数が追加されています。
  • src/pkg/time/internal_test.go: 既存のテストファイル。ForceUSPacificForTesting関数を呼び出すように変更されています。
  • src/pkg/time/time.go: timeパッケージの主要なソースファイル。locabs関数内のロジックが修正されています。
  • src/pkg/time/time_test.go: 既存のテストファイル。新しい競合状態テストTestLocationRaceが追加されています。

合計で4つのファイルが変更され、38行が追加、3行が削除されています。

変更の背景

このコミットの背景には、Go言語のtimeパッケージにおけるタイムゾーン情報の扱いに潜在する競合状態がありました。Go Issue #4622(golang.org/issue/4622)で報告されたこの問題は、複数のゴルーチンが同時にtimeパッケージの関数を呼び出し、特にローカルタイムゾーン情報がまだ初期化されていない場合に発生する可能性がありました。

Goのtimeパッケージは、システムからローカルタイムゾーン情報を一度だけロードするように設計されています。この「一度だけ」の初期化はsync.Onceというメカニズムによって保証されます。しかし、特定の条件下で、この初期化プロセス中に複数のゴルーチンが同時にタイムゾーン情報にアクセスしようとすると、予期せぬ動作やパニック(プログラムの異常終了)が発生する可能性がありました。

具体的には、Time構造体のlocフィールドがnilであるか、または初期化途中のlocalLocを指している場合に、loc.get()を呼び出す前に別のゴルーチンがlocalLocを更新してしまうと、古いまたは不完全な情報に基づいて処理が続行される可能性がありました。このコミットは、この初期化とアクセスにおけるタイミングの問題を解決し、timeパッケージの堅牢性を高めることを目的としています。

前提知識の解説

競合状態(Race Condition)

競合状態とは、複数のプロセスやスレッド(Goにおいてはゴルーチン)が共有リソース(メモリ、ファイルなど)に同時にアクセスしようとした際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。プログラムの実行結果がタイミングに依存するため、デバッグが非常に困難になることがあります。

Go言語では、ゴルーチンとチャネル、syncパッケージ(sync.Mutex, sync.WaitGroup, sync.Onceなど)を用いて競合状態を回避するための並行処理プリミティブが提供されています。

sync.Once

sync.Onceは、Go言語のsyncパッケージが提供する型で、特定の関数がプログラムの実行中に一度だけ実行されることを保証します。これは、初期化処理など、一度だけ実行されればよい処理に非常に有用です。

sync.Onceの基本的な使い方は以下の通りです。

var once sync.Once

func initialize() {
    // この関数は一度だけ実行される
    fmt.Println("初期化処理を実行")
}

func main() {
    once.Do(initialize) // initialize関数が一度だけ実行される
    once.Do(initialize) // 2回目以降の呼び出しではinitializeは実行されない
}

sync.Onceは、内部的にミューテックスとブールフラグを使用して、Doメソッドに渡された関数が一度だけ実行されることを保証します。複数のゴルーチンが同時にDoを呼び出しても、最初のゴルーチンだけが関数を実行し、他のゴルーチンはそれが完了するまで待機します。

Goのtimeパッケージとタイムゾーン

Goのtimeパッケージは、時刻と期間を扱うための機能を提供します。time.Time型は特定の時点を表し、time.Location型はタイムゾーン情報を表します。

time.Locationは、タイムゾーン名(例: "UTC", "America/Los_Angeles")と、そのタイムゾーンにおけるUTCからのオフセット(秒単位)などの情報を含みます。Goプログラムが実行される環境のローカルタイムゾーンは、通常、初回アクセス時にシステムからロードされ、timeパッケージ内部のグローバル変数(例: localLoc)にキャッシュされます。このローディングプロセスもsync.Onceによって保護されています。

技術的詳細

このコミットの核心は、timeパッケージにおけるローカルタイムゾーン情報の初期化とアクセスに関する競合状態の修正です。

Goのtimeパッケージでは、localLocというグローバル変数(*Location型)がローカルタイムゾーン情報を保持しています。このlocalLocは、sync.Onceによって一度だけ初期化されることが保証されています。しかし、Time構造体のlocabsメソッド(時刻の絶対値とタイムゾーン情報を取得する内部関数)において、t.locnilであるか、またはlocalLocを指している場合の処理に問題がありました。

元のコードでは、t.locnilの場合にutcLoc(UTCタイムゾーンを表すグローバル変数)を割り当てていました。しかし、t.loclocalLocを指している場合、特にlocalLocがまだ完全に初期化されていない途中の状態であるときに、localLocの内部状態に直接アクセスしようとすると競合状態が発生する可能性がありました。

修正後のコードでは、if l == nil || l == &localLocという条件が追加されています。

  • l == nil: これは以前と同様に、タイムゾーン情報が設定されていない場合を指します。
  • l == &localLoc: これは、Timeオブジェクトがローカルタイムゾーンを参照しているが、そのlocalLocがまだ初期化途中であるか、またはリセットされた状態である可能性を考慮しています。

この条件が真の場合、l = l.get()が呼び出されます。Location.get()メソッドは、Locationオブジェクトが指すタイムゾーン情報が適切にロードされていることを保証する役割を持っています。特にlocalLocの場合、get()sync.Onceを介してローカルタイムゾーンの初期化をトリガーし、その完了を待ちます。これにより、locabs関数が常に完全に初期化されたタイムゾーン情報にアクセスできるようになり、競合状態が解消されます。

また、テストの追加と変更も重要な側面です。

  • export_test.goの追加: このファイルは、timeパッケージの内部状態(特にlocalOncelocalLoc)をテストからリセットできるようにするための関数を提供します。これにより、タイムゾーン初期化の競合状態を再現しやすくなります。
  • TestLocationRaceの追加: このテストは、複数のゴルーチンが同時にNow().String()を呼び出すことで、タイムゾーン初期化の競合状態を意図的に引き起こし、修正が正しく機能するかを検証します。ResetLocalOnceForTest()を呼び出してsync.Onceをリセットすることで、初期化が再度行われるようにしています。

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

src/pkg/time/time.go

--- a/src/pkg/time/time.go
+++ b/src/pkg/time/time.go
@@ -261,8 +261,8 @@ func (t Time) abs() uint64 {
 // extracting both return values from a single zone lookup.
 func (t Time) locabs() (name string, offset int, abs uint64) {
 	l := t.loc
-	if l == nil {
-		l = &utcLoc
+	if l == nil || l == &localLoc {
+		l = l.get()
 	}
 	// Avoid function call if we hit the local time cache.
 	sec := t.sec + internalToUnix

src/pkg/time/export_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.

package time

import (
	"sync"
)

func ResetLocalOnceForTest() {
	localOnce = sync.Once{}
	localLoc = Location{}
}

func ForceUSPacificForTesting() {
	ResetLocalOnceForTest()
	localOnce.Do(initTestingZone)
}

src/pkg/time/time_test.go

--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -1227,6 +1227,22 @@ func TestParseDurationRoundTrip(t *testing.T) {
 	}
 }
 
+// golang.org/issue/4622
+func TestLocationRace(t *testing.T) {
+	ResetLocalOnceForTest() // reset the Once to trigger the race
+
+	c := make(chan string, 1)
+	go func() {
+		c <- Now().String()
+	}()
+	Now().String()
+	<-c
+	Sleep(100 * Millisecond)
+
+	// Back to Los Angeles for subsequent tests:
+	ForceUSPacificForTesting()
+}
+
 var (
 	t Time
 	u int64

コアとなるコードの解説

src/pkg/time/time.go の変更

locabs関数は、Timeオブジェクトの内部表現からタイムゾーン名、オフセット、および絶対時刻を取得する役割を担っています。 変更前:

	l := t.loc
	if l == nil {
		l = &utcLoc
	}

このコードは、t.locnilの場合にのみutcLocを割り当てていました。しかし、t.loclocalLocを指しているが、そのlocalLocがまだ完全に初期化されていない状態の場合、問題が発生する可能性がありました。

変更後:

	l := t.loc
	if l == nil || l == &localLoc {
		l = l.get()
	}

この修正により、t.locnilであるか、またはlocalLocのポインタと一致する場合(つまり、ローカルタイムゾーンを参照している場合)、l.get()が呼び出されるようになりました。 Location.get()メソッドは、そのLocationオブジェクトが指すタイムゾーン情報が適切にロードされていることを保証します。特にlocalLocの場合、get()sync.Onceを介してローカルタイムゾーンの初期化をトリガーし、その完了を待ちます。これにより、locabs関数が常に完全に初期化されたタイムゾーン情報にアクセスできるようになり、競合状態が解消されます。

src/pkg/time/export_test.go の新規追加

このファイルは、timeパッケージの内部状態をテスト目的で操作するためのエクスポートされた関数を提供します。

  • ResetLocalOnceForTest(): localOncesync.Onceインスタンス)を新しいゼロ値で初期化し、localLocを空のLocation構造体で初期化します。これにより、ローカルタイムゾーンの初期化が再度行われるように設定され、競合状態のテストを可能にします。
  • ForceUSPacificForTesting(): ResetLocalOnceForTest()を呼び出した後、localOnce.Do(initTestingZone)を実行します。initTestingZoneはテスト用の特定のタイムゾーン(US/Pacific)を強制的にロードする関数です。これは、テスト環境を特定のタイムゾーンに固定するために使用されます。

これらの関数は、パッケージの内部状態を外部から操作するためのものであり、通常のアプリケーションコードでは使用されません。テストの再現性と信頼性を高めるために導入されました。

src/pkg/time/time_test.goTestLocationRace

この新しいテストは、Go Issue #4622で報告された競合状態を再現し、修正が正しく機能することを確認します。

  1. ResetLocalOnceForTest(): sync.Onceをリセットし、ローカルタイムゾーンの初期化が再度行われるようにします。
  2. c := make(chan string, 1): バッファ付きチャネルを作成し、ゴルーチンからの結果を受け取ります。
  3. go func() { c <- Now().String() }(): 新しいゴルーチンを起動し、Now().String()を呼び出して時刻文字列を取得し、チャネルに送信します。Now()の呼び出しは、ローカルタイムゾーンの初期化をトリガーする可能性があります。
  4. Now().String(): メインゴルーチンでもNow().String()を呼び出します。これにより、複数のゴルーチンが同時にタイムゾーン初期化に関わる処理を実行し、競合状態を再現しようとします。
  5. <-c: ゴルーチンからの結果を待ちます。
  6. Sleep(100 * Millisecond): 短いスリープを挿入し、競合状態が完全に解決されるのを待ちます。
  7. ForceUSPacificForTesting(): 後続のテストに影響を与えないよう、タイムゾーンをUS/Pacificにリセットします。

このテストは、競合状態が発生してもパニックや不正な結果にならないことを検証します。

関連リンク

参考にした情報源リンク

  • Go Issue #4622の議論
  • Go言語のsync.Onceに関する公式ドキュメントや解説記事
  • Go言語のtimeパッケージに関する公式ドキュメント
  • Go言語における競合状態と並行処理に関する一般的な情報源
  • Go言語のソースコード(src/pkg/time/ディレクトリ)