[インデックス 14531] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnetパッケージにおけるデッドライン変数(rdeadlineとwdeadline)のデータ競合(data race)を修正するものです。具体的には、読み取りデッドラインと書き込みデッドラインが複数のゴルーチンから同時にアクセスされる際に発生しうる競合状態を解消し、スレッドセーフな操作を保証します。
コミット
commit be0d84e335a8c1b1ee420c771d7e511a79eeffd2
Author: Dave Cheney <dave@cheney.net>
Date: Fri Nov 30 18:26:51 2012 +1100
net: fix data races on deadline vars
Fixes #4434.
R=mikioh.mikioh, bradfitz, dvyukov, alex.brainman
CC=golang-dev
https://golang.org/cl/6855110
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/be0d84e335a8c1b1ee420c771d7e511a79eeffd2
元コミット内容
このコミットの元々の目的は、netパッケージ内のnetFD構造体に含まれるrdeadline(読み取りデッドライン)とwdeadline(書き込みデッドライン)というint64型の変数に対するデータ競合を修正することでした。これらの変数は、ネットワーク操作のタイムアウトを設定するために使用されますが、複数のゴルーチンから同時に読み書きされる可能性があり、適切な同期メカニズムがないと予期せぬ動作やクラッシュを引き起こす可能性がありました。
変更の背景
Go言語のnetパッケージは、ネットワークI/O操作を扱うための基盤を提供します。このパッケージでは、ソケットの読み取りおよび書き込み操作に対してタイムアウト(デッドライン)を設定する機能があります。デッドラインは、特定の時間までに操作が完了しない場合にエラーを返すようにするために使用されます。
以前の実装では、netFD構造体内のrdeadlineとwdeadlineは単純なint64型として定義されており、これらの変数へのアクセスはsync.Mutexによって保護されたrioとwioミューテックスによって部分的に同期されていました。しかし、デッドラインの値の読み取りや設定が、ミューテックスの保護範囲外で行われる場合や、ミューテックスが異なる目的で使用される場合に、データ競合が発生する可能性がありました。特に、pollServerのような非同期I/Oを扱う部分や、デッドラインのチェックを行う部分で、競合状態が問題となっていました。
この問題は、Go issue #4434として報告されており、このコミットはその修正を目的としています。データ競合は、並行処理において複数のゴルーチンが共有データに同時にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生します。このような競合は、予測不能な結果、プログラムのクラッシュ、またはセキュリティ上の脆弱性につながる可能性があります。
前提知識の解説
データ競合 (Data Race)
データ競合は、並行プログラミングにおける一般的なバグの一種です。以下の3つの条件がすべて満たされたときに発生します。
- 2つ以上のゴルーチンが同じメモリ位置にアクセスする。
- 少なくとも1つのアクセスが書き込みである。
- アクセスが同期メカニズムによって保護されていない。
データ競合が発生すると、プログラムの動作が非決定論的になり、デバッグが非常に困難になります。Go言語では、go run -raceコマンドを使用することで、実行時にデータ競合を検出することができます。
Goのnetパッケージ
netパッケージは、TCP/IP、UDP、UnixドメインソケットなどのネットワークI/Oプリミティブを提供します。このパッケージは、低レベルのネットワーク操作から高レベルのネットワークプロトコルまでをサポートし、Goアプリケーションがネットワーク通信を行うための基盤となります。netFD構造体は、ファイルディスクリプタ(またはWindowsのソケットハンドル)をラップし、ネットワーク接続の状態や関連するデッドライン情報を管理します。
sync/atomicパッケージ
sync/atomicパッケージは、低レベルのアトミック操作を提供します。アトミック操作とは、不可分な操作のことで、その操作が完了するまで他のゴルーチンから中断されることがありません。これにより、ミューテックスのような重い同期プリミティブを使用せずに、共有変数への安全なアクセスが可能になります。atomic.LoadInt64やatomic.StoreInt64は、int64型の変数をアトミックに読み書きするために使用されます。これらは、単純な読み書き操作において、データ競合を避けるための非常に効率的な方法です。
time.TimeとUnixNano()
time.Timeは、特定の時点を表すGoの型です。UnixNano()メソッドは、time.Timeの値を1970年1月1日UTCからの経過ナノ秒数としてint64で返します。デッドラインは通常、このナノ秒単位のタイムスタンプとして内部的に管理されます。
技術的詳細
このコミットの主要な技術的変更点は、デッドライン変数を単純なint64から、sync/atomicパッケージを利用してアトミックな操作をカプセル化する新しい型deadlineに置き換えたことです。
新しいdeadline型はint64のエイリアスとして定義され、以下のメソッドを持ちます。
expired() bool: 現在時刻がデッドラインを超過しているかどうかをアトミックにチェックします。value() int64: デッドラインの値をアトミックに読み取ります。set(v int64): デッドラインの値をアトミックに設定します。setTime(t time.Time):time.Time型のデッドラインをアトミックに設定します。time.Time{}(ゼロ値)の場合はデッドラインを0(設定なし)に設定します。
これにより、netFD構造体内のrdeadlineとwdeadlineがdeadline型に変更され、これらの変数へのアクセスはすべてdeadline型のメソッドを介して行われるようになりました。これらのメソッド内部では、atomic.LoadInt64とatomic.StoreInt64が使用されており、複数のゴルーチンからの同時アクセスに対してデータ競合が発生しないように保証されます。
また、fd_posix_test.goという新しいテストファイルが追加され、deadline型のsetTimeおよびexpiredメソッドの動作が検証されています。これにより、デッドラインのロジックが正しく機能することが保証されます。
Windows版のfd_windows.goでは、deadline型は定義されていますが、コメントに「データ競合が存在するかどうか不明なため、アトミック操作を使用しない」と記載されています。これは、当時のWindows環境でのレース検出ツールの成熟度や、特定のプラットフォームの特性によるものと考えられます。しかし、Unix系のシステムではアトミック操作が導入されています。
コアとなるコードの変更箇所
src/pkg/net/fd_unix.go
netFD構造体のrdeadlineとwdeadlineの型がint64からdeadlineに変更されました。deadline型が新しく定義され、expired(),value(),set(),setTime()メソッドが追加されました。これらのメソッドはsync/atomicパッケージの関数(atomic.LoadInt64,atomic.StoreInt64)を使用して、アトミックな読み書きを保証します。pollServerのAddFD、CheckDeadlines、Runメソッド内で、デッドラインの値へのアクセスがfd.rdeadline.value()やfd.wdeadline.value()のように、新しいdeadline型のメソッドを介して行われるようになりました。netFDのRead、ReadFrom、ReadMsg、Write、WriteTo、WriteMsgメソッド内で、デッドラインのチェックがfd.rdeadline.expired()やfd.wdeadline.expired()のように、新しいdeadline型のexpiredメソッドを介して行われるようになりました。これにより、デッドラインのチェックとタイムアウト処理がより簡潔かつ安全になりました。
src/pkg/net/fd_windows.go
netFD構造体のrdeadlineとwdeadlineの型がint64からdeadlineに変更されました。deadline型が新しく定義され、expired(),value(),set(),setTime()メソッドが追加されました。ただし、Unix版とは異なり、これらのメソッドはsync/atomicを使用せず、直接int64の値を操作します。これは、コメントにもあるように、当時のWindows環境でのレース検出ツールの状況を考慮したものです。Read,ReadFrom,Write,WriteTo,acceptメソッド内で、デッドラインの値へのアクセスがfd.rdeadline.value()やfd.wdeadline.value()のように、新しいdeadline型のメソッドを介して行われるようになりました。
src/pkg/net/fd_posix_test.go (新規ファイル)
deadline型のsetTimeおよびexpiredメソッドの動作を検証するための新しいテストケースが追加されました。これにより、デッドラインのロジックが正しく機能することが保証されます。
src/pkg/net/sock_posix.go および src/pkg/net/sockopt_posix.go
- デッドラインを設定する関数(
socket,setReadDeadline,setWriteDeadline,setDeadline)内で、time.Timeからint64への変換と直接代入の代わりに、fd.wdeadline.setTime(deadline)やfd.rdeadline.setTime(t)のように、新しいdeadline型のsetTimeメソッドが使用されるようになりました。これにより、デッドラインの設定もアトミックに行われるようになりました。
コアとなるコードの解説
このコミットの核心は、netFD構造体内のデッドライン変数を、データ競合から保護されたdeadline型に置き換えたことです。
// src/pkg/net/fd_unix.go
type netFD struct {
// ...
// serialize access to Read and Write methods
rio, wio sync.Mutex
// read and write deadlines
rdeadline, wdeadline deadline
// ...
}
// deadline is an atomically-accessed number of nanoseconds since 1970
// or 0, if no deadline is set.
type deadline int64
func (d *deadline) expired() bool {
t := d.value()
return t > 0 && time.Now().UnixNano() >= t
}
func (d *deadline) value() int64 {
return atomic.LoadInt64((*int64)(d)) // アトミックな読み取り
}
func (d *deadline) set(v int64) {
atomic.StoreInt64((*int64)(d), v) // アトミックな書き込み
}
func (d *deadline) setTime(t time.Time) {
if t.IsZero() {
d.set(0)
} else {
d.set(t.UnixNano())
}
}
この変更により、rdeadlineとwdeadlineへのすべてのアクセスは、deadline型のメソッドを介して行われるようになります。特にvalue()とset()メソッドはsync/atomicパッケージの関数を使用しているため、これらのデッドライン変数の読み書きは常にアトミックに実行されます。これにより、複数のゴルーチンが同時にデッドラインにアクセスしても、データ競合が発生せず、一貫性のある値が保証されます。
例えば、以前はif time.Now().UnixNano() >= fd.rdeadlineのように直接int64の値を参照していましたが、これがif fd.rdeadline.expired()に置き換えられました。expired()メソッド内部ではvalue()が呼ばれ、アトミックにデッドラインの値が取得されます。これにより、デッドラインのチェック中に別のゴルーチンがデッドラインの値を変更しても、競合状態が発生しなくなります。
この修正は、Goの並行処理モデルにおいて、共有状態を安全に管理するためのベストプラクティスを示しています。単純な変数の読み書きであっても、複数のゴルーチンからアクセスされる可能性がある場合は、sync/atomicのようなアトミック操作を使用するか、sync.Mutexなどのより高レベルの同期プリミティブを使用することが重要です。
関連リンク
- Go issue #4434: https://github.com/golang/go/issues/4434
- Go CL 6855110: https://golang.org/cl/6855110
参考にした情報源リンク
- Go言語の
sync/atomicパッケージのドキュメント: https://pkg.go.dev/sync/atomic - Go言語の
timeパッケージのドキュメント: https://pkg.go.dev/time - Go言語におけるデータ競合の解説(公式ブログなど)
- The Go Memory Model: https://go.dev/ref/mem
- Go Concurrency Patterns: https://go.dev/blog/concurrency-patterns
- Data Races in Go: https://go.dev/blog/race-detector (Go Race Detectorに関する記事)
- Go言語の
netパッケージのドキュメント: https://pkg.go.dev/net