[インデックス 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