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

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

このコミットは、Go言語の標準ライブラリである net パッケージにおけるデッドライン(タイムアウト)変数のデータ競合を修正するものです。具体的には、読み込みおよび書き込みデッドラインの設定とアクセスにおいて発生していた競合状態を解消し、より堅牢な並行処理を実現しています。

コミット

commit 9fb96991e63033ba963c7b1eff10e5c4f5a93b0a
Author: Dave Cheney <dave@cheney.net>
Date:   Wed Dec 5 15:59:01 2012 +1100

    net: fix data races on deadline vars
    
    Fixes #4434.
    
    This proposal replaces the previous CL 6855110. Due to issue 599, 64-bit atomic operations should probably be avoided, so use a sync.Mutex instead.
    
    Benchmark comparisons against 025b9d070a85 on linux/386:
    
    CL 6855110:
    
    benchmark                        old ns/op    new ns/op    delta
    BenchmarkTCPOneShot                 710024       727409   +2.45%
    BenchmarkTCPOneShotTimeout          758178       768620   +1.38%
    BenchmarkTCPPersistent              223464       228058   +2.06%
    BenchmarkTCPPersistentTimeout       234494       242600   +3.46%
    
    This proposal:
    
    benchmark                        old ns/op    new ns/op    delta
    BenchmarkTCPOneShot                 710024       718492   +1.19%
    BenchmarkTCPOneShotTimeout          758178       748783   -1.24%
    BenchmarkTCPPersistent              223464       227628   +1.86%
    BenchmarkTCPPersistentTimeout       234494       238321   +1.63%
    
    R=rsc, dvyukov, mikioh.mikioh, alex.brainman, bradfitz
    CC=golang-dev, remyoudompheng
    https://golang.org/cl/6866050

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

https://github.com/golang/go/commit/9fb96991e63033ba963c7b1eff10e5c4f5a93b0a

元コミット内容

このコミットは、Go言語の net パッケージにおけるデッドライン(タイムアウト)処理に関するデータ競合を修正するものです。以前の試み(CL 6855110)では64ビットアトミック操作を使用しようとしましたが、GoのIssue 599(32ビットシステムでの64ビットアトミック操作に関する問題)のため、代わりに sync.Mutex を使用するアプローチに変更されました。

ベンチマーク結果も示されており、この変更によるパフォーマンスへの影響は、以前のアトミック操作を用いたCLと比較して、わずかな変動(一部で改善、一部で微増)に留まっていることが示されています。

変更の背景

この変更の主な背景は、Go言語の net パッケージにおけるネットワーク接続の読み込みおよび書き込みデッドライン(タイムアウト)の設定とチェックにおいて、データ競合が発生していたことです。データ競合は、複数のゴルーチンが同時に共有データにアクセスし、少なくとも1つのアクセスが書き込みである場合に発生し、予測不能な動作やバグを引き起こす可能性があります。

特に、GoのIssue 4434「net: data race on deadline vars」で報告された問題に対処しています。この問題は、netFD 構造体内の rdeadline (読み込みデッドライン) および wdeadline (書き込みデッドライン) 変数へのアクセスが適切に同期されていなかったために発生していました。

また、以前の修正試み(CL 6855110)では64ビットアトミック操作を使用する方針でしたが、GoのIssue 599「sync/atomic: 64-bit operations on 32-bit systems are not atomic」が指摘するように、32ビットシステム上での64ビットアトミック操作が真にアトミックではないという問題が存在しました。このため、より安全で移植性の高い sync.Mutex を使用するアプローチに切り替える必要がありました。

前提知識の解説

データ競合 (Data Race)

データ競合とは、複数の並行実行される処理(Goにおいてはゴルーチン)が、共有メモリ上の同じ変数に同時にアクセスし、そのうち少なくとも1つのアクセスが書き込み操作である場合に発生する競合状態です。データ競合が発生すると、プログラムの実行結果がアクセス順序に依存するようになり、予測不能な動作、クラッシュ、または誤った結果を引き起こす可能性があります。Go言語では、データ競合を検出するためのツール(Race Detector)が提供されています。

デッドライン (Deadline)

ネットワークプログラミングにおいて、デッドラインは操作が完了しなければならない絶対的な時刻を指します。例えば、ネットワークからのデータの読み込みや書き込みに時間がかかりすぎる場合、デッドラインを設定することで、その操作を途中で打ち切り、タイムアウトエラーを発生させることができます。これにより、アプリケーションが応答不能になるのを防ぎ、リソースの無駄な消費を抑えることができます。Goの net パッケージでは、SetReadDeadlineSetWriteDeadline メソッドを通じてデッドラインを設定できます。

Goの並行処理プリミティブ

Go言語は、並行処理をサポートするための強力なプリミティブを提供しています。

  • sync.Mutex: ミューテックス(相互排他ロック)は、共有リソースへのアクセスを一度に1つのゴルーチンに制限するためのメカニズムです。Lock() メソッドでロックを取得し、Unlock() メソッドでロックを解放します。これにより、クリティカルセクション(共有データにアクセスするコード部分)への同時アクセスを防ぎ、データ競合を回避できます。
  • sync/atomic パッケージ: このパッケージは、低レベルのアトミック操作を提供します。アトミック操作は、中断されることなく単一の不可分な操作として実行されることが保証されます。これにより、ロックを使用せずに特定の共有変数へのアクセスを安全に行うことができます。しかし、本コミットの背景にあるIssue 599のように、特定のプラットフォーム(32ビットシステムでの64ビット操作など)では、ハードウェアの制約により真のアトミック性が保証されない場合があります。

time.TimeUnixNano()

  • time.Time: Go言語で時刻を扱うための型です。特定の時点を表します。
  • UnixNano(): time.Time 型のメソッドで、1970年1月1日UTCからの経過時間をナノ秒単位で int64 型で返します。デッドラインを数値として表現し、比較するために使用されます。

time.Time.IsZero()

time.Time 型のメソッドで、その時刻がGoの time.Time 型のゼロ値(time.Time{}、つまり「設定されていない」状態)であるかどうかを判定します。デッドラインが設定されていない状態を表現するために利用されます。

技術的詳細

このコミットの主要な技術的変更は、デッドラインの管理方法を根本的に変更した点にあります。

  1. deadline 構造体の導入: 以前は netFD 構造体内で rdeadlinewdeadline が直接 int64 型で保持され、その同期は外部の sync.Mutex や、場合によってはアトミック操作に依存していました。このコミットでは、net/net.go に新しい deadline 構造体が導入されました。

    type deadline struct {
        sync.Mutex
        val int64
    }
    

    この構造体は、デッドラインのナノ秒値 (val) と、その値へのアクセスを保護するための専用の sync.Mutex をカプセル化しています。これにより、デッドラインの値へのすべてのアクセス(読み取りと書き込み)が、このミューテックスによって保護されるようになります。

  2. デッドライン操作のメソッド化: deadline 構造体には、デッドラインの操作を安全に行うための以下のメソッドが追加されました。

    • expired() bool: 現在の時刻がデッドラインを超過しているかどうかを判定します。内部で value() を呼び出し、time.Now().UnixNano() と比較します。
    • value() int64: デッドラインのナノ秒値を取得します。ミューテックスをロックしてから値を読み取り、アンロックします。
    • set(v int64): デッドラインのナノ秒値を設定します。ミューテックスをロックしてから値を書き込み、アンロックします。
    • setTime(t time.Time): time.Time 型のデッドラインを設定します。t.IsZero() でデッドラインが設定されていない場合(ゼロ値の場合)は val0 に設定し、それ以外の場合は t.UnixNano()val に設定します。この操作もミューテックスによって保護されます。
  3. netFD 構造体の変更: src/pkg/net/fd_unix.go および src/pkg/net/fd_windows.go 内の netFD 構造体において、rdeadlinewdeadline の型が int64 から新しく定義された deadline 型に変更されました。

    // 変更前:
    // rdeadline int64
    // rio       sync.Mutex
    // wdeadline int64
    // wio       sync.Mutex
    
    // 変更後:
    // rio, wio sync.Mutex // Read and Write methods access serialization
    // rdeadline, wdeadline deadline // read and write deadlines
    

    riowio は引き続き Read および Write メソッド全体のシリアライズに使用されますが、デッドラインの値自体へのアクセスは deadline 構造体内のミューテックスによって保護されるようになりました。

  4. デッドラインチェックロジックの変更: netFDRead, ReadFrom, ReadMsg, Write, WriteTo, WriteMsg メソッド内で、デッドラインのチェックロジックが fd.rdeadline.expired()fd.wdeadline.expired() の呼び出しに置き換えられました。これにより、デッドラインのチェックがカプセル化され、安全に行われるようになります。

  5. デッドライン設定ロジックの変更: src/pkg/net/sock_posix.go および src/pkg/net/sockopt_posix.go 内の setReadDeadline, setWriteDeadline, setDeadline 関数が、新しい deadline 構造体の setTime メソッドを使用するように変更されました。これにより、デッドラインの設定も安全な方法で行われるようになります。

この変更により、デッドラインの値へのアクセスが常に deadline 構造体内のミューテックスによって保護されるため、複数のゴルーチンが同時にデッドラインを読み書きしようとしてもデータ競合が発生しなくなります。また、32ビットシステムでの64ビットアトミック操作の潜在的な問題を回避し、より堅牢な実装が実現されました。

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

src/pkg/net/net.go (新規追加)

// deadline is an atomically-accessed number of nanoseconds since 1970
// or 0, if no deadline is set.
type deadline struct {
	sync.Mutex
	val int64
}

func (d *deadline) expired() bool {
	t := d.value()
	return t > 0 && time.Now().UnixNano() >= t
}

func (d *deadline) value() (v int64) {
	d.Lock()
	v = d.val
	d.Unlock()
	return
}

func (d *deadline) set(v int64) {
	d.Lock()
	d.val = v
	d.Unlock()
}

func (d *deadline) setTime(t time.Time) {
	if t.IsZero() {
		d.set(0)
	} else {
		d.set(t.UnixNano())
	}
}

src/pkg/net/fd_unix.go および src/pkg/net/fd_windows.go (変更)

netFD 構造体の rdeadlinewdeadline の型が int64 から deadline に変更。

type netFD struct {
	// ...
	// 変更前:
	// rdeadline int64
	// rio       sync.Mutex
	// wdeadline int64
	// wio       sync.Mutex

	// 変更後:
	// serialize access to Read and Write methods
	rio, wio sync.Mutex

	// read and write deadlines
	rdeadline, wdeadline deadline
	// ...
}

デッドラインのチェック箇所が fd.rdeadline.expired()fd.wdeadline.expired() に変更。

// 例: Read メソッド内
// 変更前:
// if fd.rdeadline > 0 {
//     if time.Now().UnixNano() >= fd.rdeadline {
//         err = errTimeout
//         break
//     }
// }

// 変更後:
if fd.rdeadline.expired() {
    err = errTimeout
    break
}

src/pkg/net/sock_posix.go および src/pkg/net/sockopt_posix.go (変更)

デッドライン設定関数が deadline 構造体の setTime メソッドを使用するように変更。

// 例: setReadDeadline 関数
// 変更前:
// func setReadDeadline(fd *netFD, t time.Time) error {
//     if t.IsZero() {
//         fd.rdeadline = 0
//     } else {
//         fd.rdeadline = t.UnixNano()
//     }
//     return nil
// }

// 変更後:
func setReadDeadline(fd *netFD, t time.Time) error {
	fd.rdeadline.setTime(t)
	return nil
}

コアとなるコードの解説

このコミットの核心は、デッドラインの値を直接 int64 として扱うのではなく、deadline という新しい構造体でカプセル化し、その構造体自身が sync.Mutex を用いて内部の val (ナノ秒単位のデッドライン時刻) へのアクセスを同期するという設計変更にあります。

  1. deadline 構造体:

    • sync.Mutex: このミューテックスは、val フィールドへのすべての読み書き操作を保護するために使用されます。これにより、複数のゴルーチンが同時にデッドラインの値にアクセスしようとしても、ミューテックスが排他制御を行うため、データ競合が発生しません。
    • val int64: 1970年1月1日UTCからのナノ秒単位の絶対時刻を表します。デッドラインが設定されていない場合は 0 になります。
  2. expired() メソッド:

    • このメソッドは、デッドラインが既に経過しているかどうかを効率的かつ安全にチェックします。
    • d.value() を呼び出すことで、ミューテックスによって保護された val の現在の値を取得します。
    • t > 0 の条件は、デッドラインが実際に設定されている場合(ゼロ値ではない場合)にのみチェックを行うことを保証します。デッドラインが設定されていない場合(val0 の場合)、expired()false を返します。
    • time.Now().UnixNano() >= t は、現在の時刻がデッドライン時刻以上であるかを比較し、デッドラインが過ぎているかを判断します。
  3. value() メソッド:

    • d.Lock()d.Unlock() を使用して、val の読み取り操作を保護します。これにより、val が読み取られている最中に他のゴルーチンが val を変更するのを防ぎ、常に一貫性のある値を取得できます。
  4. set(v int64) メソッド:

    • d.Lock()d.Unlock() を使用して、val の書き込み操作を保護します。これにより、複数のゴルーチンが同時に val を設定しようとしても、データ競合が発生しません。
  5. setTime(t time.Time) メソッド:

    • time.Time 型の引数 t を受け取り、それをナノ秒単位の int64 値に変換して set() メソッドに渡します。
    • t.IsZero() をチェックすることで、time.Time{}(ゼロ値)が渡された場合にはデッドラインを 0 に設定し、「デッドラインなし」の状態を表現します。これは、デッドラインを解除する際によく使われるパターンです。
    • このメソッドも内部で set() を呼び出すため、ミューテックスによる保護が適用されます。

この設計により、netFD 構造体は int64 のデッドライン変数とそれに対応するミューテックスを個別に管理する代わりに、deadline 型のフィールドを持つだけでよくなります。デッドラインの値へのアクセスや変更はすべて deadline 型のメソッドを通じて行われるため、開発者は明示的にミューテックスをロック・アンロックする必要がなくなり、コードの可読性と安全性が向上します。

特に、32ビットシステムでの64ビットアトミック操作の信頼性の問題(Issue 599)を回避するために sync.Mutex が選択されたことは重要です。sync.Mutex は、プラットフォームに依存しない形で排他制御を保証するため、より堅牢な解決策となります。

関連リンク

参考にした情報源リンク