[インデックス 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.loc
がnil
であるか、またはlocalLoc
を指している場合の処理に問題がありました。
元のコードでは、t.loc
がnil
の場合にutcLoc
(UTCタイムゾーンを表すグローバル変数)を割り当てていました。しかし、t.loc
がlocalLoc
を指している場合、特に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
パッケージの内部状態(特にlocalOnce
とlocalLoc
)をテストからリセットできるようにするための関数を提供します。これにより、タイムゾーン初期化の競合状態を再現しやすくなります。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.loc
がnil
の場合にのみutcLoc
を割り当てていました。しかし、t.loc
がlocalLoc
を指しているが、そのlocalLoc
がまだ完全に初期化されていない状態の場合、問題が発生する可能性がありました。
変更後:
l := t.loc
if l == nil || l == &localLoc {
l = l.get()
}
この修正により、t.loc
がnil
であるか、またはlocalLoc
のポインタと一致する場合(つまり、ローカルタイムゾーンを参照している場合)、l.get()
が呼び出されるようになりました。
Location.get()
メソッドは、そのLocation
オブジェクトが指すタイムゾーン情報が適切にロードされていることを保証します。特にlocalLoc
の場合、get()
はsync.Once
を介してローカルタイムゾーンの初期化をトリガーし、その完了を待ちます。これにより、locabs
関数が常に完全に初期化されたタイムゾーン情報にアクセスできるようになり、競合状態が解消されます。
src/pkg/time/export_test.go
の新規追加
このファイルは、time
パッケージの内部状態をテスト目的で操作するためのエクスポートされた関数を提供します。
ResetLocalOnceForTest()
:localOnce
(sync.Once
インスタンス)を新しいゼロ値で初期化し、localLoc
を空のLocation
構造体で初期化します。これにより、ローカルタイムゾーンの初期化が再度行われるように設定され、競合状態のテストを可能にします。ForceUSPacificForTesting()
:ResetLocalOnceForTest()
を呼び出した後、localOnce.Do(initTestingZone)
を実行します。initTestingZone
はテスト用の特定のタイムゾーン(US/Pacific)を強制的にロードする関数です。これは、テスト環境を特定のタイムゾーンに固定するために使用されます。
これらの関数は、パッケージの内部状態を外部から操作するためのものであり、通常のアプリケーションコードでは使用されません。テストの再現性と信頼性を高めるために導入されました。
src/pkg/time/time_test.go
の TestLocationRace
この新しいテストは、Go Issue #4622で報告された競合状態を再現し、修正が正しく機能することを確認します。
ResetLocalOnceForTest()
:sync.Once
をリセットし、ローカルタイムゾーンの初期化が再度行われるようにします。c := make(chan string, 1)
: バッファ付きチャネルを作成し、ゴルーチンからの結果を受け取ります。go func() { c <- Now().String() }()
: 新しいゴルーチンを起動し、Now().String()
を呼び出して時刻文字列を取得し、チャネルに送信します。Now()
の呼び出しは、ローカルタイムゾーンの初期化をトリガーする可能性があります。Now().String()
: メインゴルーチンでもNow().String()
を呼び出します。これにより、複数のゴルーチンが同時にタイムゾーン初期化に関わる処理を実行し、競合状態を再現しようとします。<-c
: ゴルーチンからの結果を待ちます。Sleep(100 * Millisecond)
: 短いスリープを挿入し、競合状態が完全に解決されるのを待ちます。ForceUSPacificForTesting()
: 後続のテストに影響を与えないよう、タイムゾーンをUS/Pacificにリセットします。
このテストは、競合状態が発生してもパニックや不正な結果にならないことを検証します。
関連リンク
- Go Issue #4622: https://github.com/golang/go/issues/4622
- Go CL 7103046: https://golang.org/cl/7103046 (これは古いGerritのリンクであり、現在はGitHubのコミットページにリダイレクトされるか、関連するコミットとして参照されます)
参考にした情報源リンク
- Go Issue #4622の議論
- Go言語の
sync.Once
に関する公式ドキュメントや解説記事 - Go言語の
time
パッケージに関する公式ドキュメント - Go言語における競合状態と並行処理に関する一般的な情報源
- Go言語のソースコード(
src/pkg/time/
ディレクトリ)