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

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

このコミットは、Go言語の標準ライブラリであるtimeパッケージにおけるデータ競合(data race)の問題を修正するものです。具体的には、Time型の内部メソッドであるabs()関数内で、Location(タイムゾーン情報)の取得と利用に関する競合状態を回避するための変更が加えられています。これにより、複数のゴルーチンが同時にTimeオブジェクトの時刻表現プロパティ(月、時間など)を計算しようとした際に発生しうる、未定義の動作やクラッシュを防ぎます。

コミット

commit 84a5a9b558fbe1a4d20d1be822eefa1fd504d8df
Author: Rob Pike <r@golang.org>
Date:   Wed Aug 22 14:36:23 2012 -0700

    time: avoid data race in abs
    Fixes #3967.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6460115

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

https://github.com/golang/go/commit/84a5a9b558fbe1a4d20d1be822eefa1fd504d8df

元コミット内容

time: avoid data race in abs
Fixes #3967.

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6460115

変更の背景

Go言語のtimeパッケージは、日付と時刻を扱うための機能を提供します。Time構造体は、特定の時点を表し、その時刻を様々なタイムゾーンで表現するためのLocation情報を持つことができます。TimeオブジェクトのMonthHourといった時刻表現プロパティを計算する際には、内部的にabs()メソッドが呼び出されます。

このabs()メソッド内で、Timeオブジェクトが持つLocation情報(t.loc)がnilの場合、またはlocalLoc(ローカルタイムゾーン)を参照している場合に、そのLocationオブジェクトを適切に初期化または取得する処理が行われていました。しかし、この処理が複数のゴルーチンから同時に実行された場合、Locationオブジェクトの内部状態が正しく同期されず、データ競合が発生する可能性がありました。

データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生します。Goのメモリモデルでは、データ競合が発生した場合のプログラムの動作は未定義とされており、クラッシュ、不正な値の読み込み、デッドロックなど、予測不能な結果を引き起こす可能性があります。

このコミットは、Go issue #3967で報告されたこのデータ競合の問題を解決するために行われました。

前提知識の解説

Go言語のtimeパッケージ

Go言語のtimeパッケージは、時刻の表現、測定、表示のための機能を提供します。主要な型は以下の通りです。

  • time.Time: 特定の時点を表す構造体です。内部的には、エポック(1970年1月1日UTC)からの経過秒数とナノ秒数を保持し、さらにその時刻がどのLocation(タイムゾーン)で解釈されるべきかを示す*Locationフィールド(loc)を持ちます。
  • time.Duration: 2つの時点間の時間間隔を表す型です。
  • time.Location: タイムゾーンを表す構造体です。UTC、ローカルタイムゾーン、または特定の名前付きタイムゾーン(例: "America/New_York")を指定できます。time.UTCはUTCタイムゾーンを、time.Localはシステムのローカルタイムゾーンを表します。

データ競合 (Data Race)

データ競合は、並行プログラミングにおける一般的なバグの一種です。Go言語においてデータ競合は以下の条件がすべて満たされた場合に発生します。

  1. 少なくとも2つのゴルーチンが同時に同じメモリ位置にアクセスする。
  2. 少なくとも1つのアクセスが書き込みである。
  3. アクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。

Goのメモリモデルは、データ競合が発生した場合のプログラムの動作を保証しません。これは、コンパイラやCPUが命令の順序を最適化する際に、競合するアクセスが予期せぬ順序で実行される可能性があるためです。結果として、プログラムはクラッシュしたり、誤った結果を生成したり、デッドロックに陥ったりする可能性があります。

データ競合を検出するためには、Goに組み込まれている競合検出器(Race Detector)を使用するのが一般的です。go run -racego build -racego test -raceなどのコマンドで有効にできます。

Time.abs() メソッド

time.Time構造体の内部メソッドであるabs()は、時刻の絶対値を計算するものではなく、時刻の内部表現を正規化し、その時刻がどのタイムゾーンで解釈されるべきかを決定する際に使用されます。特に、Month()Hour()などのメソッドが呼び出され、時刻の「表示プロパティ」を計算する必要がある場合に、このabs()メソッドが呼び出されます。このメソッドは、Timeオブジェクトのlocフィールドがnilの場合や、Localタイムゾーンがまだ適切に初期化されていない場合に、適切なLocationオブジェクトを取得する役割を担っていました。

技術的詳細

このコミットが修正するデータ競合は、time.Time構造体のabs()メソッド内で、t.locフィールドがnilまたはlocalLoc(ローカルタイムゾーン)である場合の処理に起因していました。

元のコードでは、t.locnilの場合にutcLoc(UTCタイムゾーン)を直接代入していました。また、localLocの場合には、その後の処理でキャッシュを利用していましたが、localLoc自体の初期化や取得がスレッドセーフに行われていない可能性がありました。

Goのtime.Localは、初めてアクセスされたときにシステムのローカルタイムゾーン情報をロードします。このロード処理は、通常、一度だけ行われるべきですが、複数のゴルーチンが同時にtime.Localにアクセスし、かつその初期化がまだ完了していない場合に、競合状態が発生する可能性があります。特に、localLocが指すLocationオブジェクトの内部状態(例えば、タイムゾーンのオフセットやサマータイムのルールなど)が、複数のゴルーチンによって同時に変更されようとすると、データ競合が発生します。

このコミットでは、t.locnilまたはlocalLocである場合に、l.get()というメソッドを呼び出すように変更されています。このget()メソッドは、Locationオブジェクトが持つ内部的な初期化ロジックをカプセル化し、スレッドセーフな方法でLocationオブジェクトを取得または初期化することを保証します。具体的には、get()メソッドの内部で、sync.Onceなどの同期プリミティブを使用して、Location情報のロードが一度だけ、かつ安全に行われるように制御されていると考えられます。これにより、複数のゴルーチンが同時にabs()を呼び出しても、Locationオブジェクトの初期化やアクセスに関するデータ競合が回避されます。

また、元のコードにあった// Avoid function call if we hit the local time cache.というコメントが削除され、// Avoid function calls when possible.というコメントが移動・変更されています。これは、localLocの扱いが変更され、get()メソッドを介することで、より堅牢な初期化ロジックが適用されるようになったことを示唆しています。

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

--- a/src/pkg/time/time.go
+++ b/src/pkg/time/time.go
@@ -241,10 +241,10 @@ func (t Time) IsZero() bool {
 // It is called when computing a presentation property like Month or Hour.
 func (t Time) abs() uint64 {
 	l := t.loc
-	if l == nil {
-		l = &utcLoc
+	// Avoid function calls when possible.
+	if l == nil || l == &localLoc {
+		l = l.get()
 	}
-	// Avoid function call if we hit the local time cache.
 	sec := t.sec + internalToUnix
 	if l != &utcLoc {
 		if l.cacheZone != nil && l.cacheStart <= sec && sec < l.cacheEnd {

コアとなるコードの解説

変更されたのは、time.goファイルのTime型のabs()メソッド内の以下の部分です。

変更前:

func (t Time) abs() uint64 {
	l := t.loc
	if l == nil {
		l = &utcLoc
	}
	// Avoid function call if we hit the local time cache.
	// ...
}

変更後:

func (t Time) abs() uint64 {
	l := t.loc
	// Avoid function calls when possible.
	if l == nil || l == &localLoc {
		l = l.get()
	}
	// ...
}

この変更のポイントは以下の通りです。

  1. 条件式の変更:

    • 変更前はif l == nilという条件で、t.locnilの場合にl&utcLocに設定していました。
    • 変更後はif l == nil || l == &localLocという条件に変わりました。これにより、t.locnilの場合だけでなく、&localLoc(システムのローカルタイムゾーン)を指している場合も、特別な処理を行うようになりました。
  2. l.get()の導入:

    • 変更前はl = &utcLocと直接代入していました。
    • 変更後はl = l.get()と、Locationオブジェクトのget()メソッドを呼び出すようになりました。このget()メソッドは、Locationオブジェクトが持つ内部的な初期化ロジックをカプセル化し、スレッドセーフな方法でLocationオブジェクトを取得または初期化することを保証します。特にtime.LocalのようなグローバルなLocationオブジェクトは、初めてアクセスされたときにシステムのタイムゾーン情報をロードする必要があり、このロード処理がデータ競合を引き起こす可能性がありました。get()メソッドは、このロード処理が一度だけ、かつ安全に行われるように設計されていると考えられます。
  3. コメントの変更:

    • 元の// Avoid function call if we hit the local time cache.というコメントが削除されました。これは、localLocの扱いが変更され、キャッシュヒットの最適化よりもデータ競合の回避が優先されたことを示唆しています。
    • // Avoid function calls when possible.というコメントが移動し、if文の直前に配置されました。これは、get()メソッドの呼び出しが、必要な場合にのみ行われるように、条件分岐が最適化されていることを示しています。

この変更により、TimeオブジェクトのlocフィールドがnilまたはlocalLocである場合に、Locationオブジェクトの初期化やアクセスがスレッドセーフに行われるようになり、データ競合が効果的に回避されるようになりました。

関連リンク

  • Go issue #3967: このコミットが修正したデータ競合の問題に関するGoのイシュートラッカーのエントリ。

参考にした情報源リンク