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

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

このコミットは、Go言語の標準ライブラリnetパッケージにおける、Windows環境でのファイルディスクリプタ(fd_windows.go)の挙動に関する修正です。具体的には、ネットワークI/O操作を開始する前に、設定されたデッドライン(期限)が既に過ぎている場合に、そのI/O操作を早期に中止し、無駄な処理を避けるための変更が加えられています。これにより、不要なシステムコールやリソースの消費を防ぎ、より効率的かつ堅牢なネットワーク処理を実現します。

コミット

commit 747dda9767d3da479900ad180b11a73f78f0412e
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Sun Nov 25 10:02:57 2012 +1100

    net: do not start io if deadline has passed
    
    R=golang-dev, mikioh.mikioh, minux.ma, bradfitz
    CC=golang-dev
    https://golang.org/cl/6851098

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

https://github.com/golang/go/commit/747dda9767d3da479900ad180b11a73f78f0412e

元コミット内容

net: do not start io if deadline has passed

このコミットの目的は、ネットワークI/O操作を開始する際に、その操作に設定されたデッドラインが既に経過している場合、I/O操作自体を開始しないようにすることです。これにより、デッドライン切れのI/O操作が不必要に実行されることを防ぎ、リソースの無駄遣いを削減し、エラーハンドリングを改善します。

変更の背景

Go言語のnetパッケージでは、ネットワーク接続に対して読み取り(Read)や書き込み(Write)のデッドラインを設定することができます。これは、特定の時間内にI/O操作が完了しない場合にタイムアウトエラーを発生させるための機能です。

このコミットが導入される前の実装では、I/O操作を開始する際に、その操作のデッドラインが既に過ぎているかどうかを事前にチェックしていませんでした。そのため、デッドラインが過去の時間に設定されていたり、I/O操作の準備中に時間が経過してデッドラインを過ぎてしまったりした場合でも、一度I/O操作を開始しようとしていました。

このような挙動は、以下のような問題を引き起こす可能性がありました。

  1. 無駄なシステムコールとリソース消費: デッドラインが過ぎているにもかかわらずI/O操作を開始しようとすると、OSへのシステムコールが発生し、不必要なリソース(CPU時間、メモリなど)が消費されます。これは特に、高負荷な環境や多数のコネクションを扱うサーバーアプリケーションにおいて、パフォーマンスの低下やリソース枯渇の原因となり得ます。
  2. 遅延と非効率性: デッドラインが過ぎたI/O操作は、結局タイムアウトエラーで終了します。しかし、そのエラーに至るまでに不必要な処理が実行されるため、全体的な処理の遅延や非効率性が発生します。
  3. 予測不能な挙動: デッドラインが過ぎたI/Oが開始されることで、アプリケーションのロジックが期待しないタイミングでタイムアウトエラーを受け取る可能性があり、デバッグやエラーハンドリングが複雑になることがあります。

このコミットは、これらの問題を解決するために、I/O操作を開始する前にデッドラインの有効性をチェックし、既にデッドラインが過ぎている場合は即座にタイムアウトエラーを返すように変更しました。これにより、より効率的で予測可能なネットワークI/O処理が実現されます。

前提知識の解説

このコミットを理解するためには、以下の概念について知っておく必要があります。

  • ネットワークI/Oとデッドライン:
    • I/O (Input/Output): コンピュータシステムと外部デバイス(この場合はネットワーク)との間でデータをやり取りする操作全般を指します。ネットワークI/Oは、データの送受信を意味します。
    • デッドライン (Deadline): 特定の操作が完了しなければならない期限のことです。Go言語のnetパッケージでは、SetReadDeadlineSetWriteDeadlineなどのメソッドを使って、読み取りや書き込み操作の最大許容時間を設定できます。この時間を過ぎると、I/O操作は自動的に中断され、タイムアウトエラー(os.ErrDeadlineExceededまたはnet.OpErrorにラップされたエラー)が返されます。
  • time.Now().UnixNano():
    • Go言語のtimeパッケージに含まれる関数で、現在の時刻をUnixエポック(1970年1月1日UTC)からの経過時間をナノ秒単位でint64型で返します。デッドラインの計算や経過時間の測定によく使用されます。
  • time.NewTimer(duration):
    • 指定されたduration(期間)が経過した後に、time.Time型の値を送信するチャネルを返すタイマーを作成します。このタイマーは、I/O操作がデッドラインまでに完了しない場合にタイムアウトを検出するために使用されます。
    • defer t.Stop(): タイマーが不要になった際にリソースを解放するために、Stop()メソッドを呼び出すことが重要です。
  • OpError:
    • Go言語のnetパッケージで発生するネットワーク操作のエラーを表す構造体です。どの操作(Op)、ネットワークタイプ(Net)、アドレス(SourceAddr)、そして根本的なエラー(Err)によってエラーが発生したかを詳細に含みます。このコミットでは、デッドラインが過ぎた場合にerrTimeout(内部的にはos.ErrDeadlineExceeded)をラップしたOpErrorを返します。
  • fd_windows.go:
    • Go言語のnetパッケージの内部実装ファイルの一つで、Windowsオペレーティングシステムにおけるファイルディスクリプタ(ソケット)のI/O処理を扱います。Goのネットワーク機能はプラットフォームごとに最適化されており、このファイルはWindows固有のI/Oメカニズム(例: I/O完了ポートなど)と連携するためのロジックを含んでいます。このコミットの変更は、Windows環境でのネットワークI/Oのデッドライン処理に特化しています。
  • ioSrvExecIO:
    • ioSrvは、Goのnetパッケージ内部でI/O操作を管理するための構造体です。
    • ExecIOは、実際のI/O操作(読み取り、書き込みなど)を実行し、デッドライン処理を行うためのメソッドです。このメソッドが、今回の変更の主要な対象となっています。

技術的詳細

このコミットの技術的な核心は、src/pkg/net/fd_windows.goファイルのExecIO関数におけるデッドラインチェックのロジックの変更です。

変更前は、デッドラインが設定されている場合、まずI/O操作(oi.Submit()またはo.fd.Read/Write)を開始し、その後にタイマーを設定してデッドラインを監視していました。このアプローチでは、I/O操作を開始する時点で既にデッドラインが過ぎていたとしても、一度はI/O操作が試行されていました。

変更後は、I/O操作を開始するに、デッドラインが有効であるかどうかのチェックが追加されました。

  1. deltaの計算:

    var delta int64
    if deadline != 0 {
        delta = deadline - time.Now().UnixNano()
        if delta <= 0 {
            return 0, &OpError{oi.Name(), o.fd.net, o.fd.laddr, errTimeout}
        }
    }
    
    • deadline != 0は、デッドラインが設定されているかどうかを確認します(0はデッドラインが設定されていないことを意味します)。
    • delta = deadline - time.Now().UnixNano(): 現在時刻からデッドラインまでの残り時間をナノ秒単位で計算します。deltaが正の値であれば、まだデッドラインまで時間があることを意味します。
    • if delta <= 0: ここが重要な変更点です。もしdeltaが0以下、つまりデッドラインが既に過ぎているか、現在時刻と同時である場合、I/O操作を開始せずに、即座にタイムアウトエラーを返します。
      • return 0, &OpError{oi.Name(), o.fd.net, o.fd.laddr, errTimeout}: 0バイトの読み書きと、OpError型のタイムアウトエラーを返します。errTimeoutは内部的にos.ErrDeadlineExceededをラップしています。
  2. I/O操作の開始:

    // Start IO.
    if canCancelIO {
        err = oi.Submit()
    } else {
        // ... (既存のI/O開始ロジック)
    }
    

    この// Start IO.コメント以下のI/O開始ロジックは、上記のデッドラインチェックを通過した場合にのみ実行されます。これにより、デッドラインが過ぎたI/O操作が不必要に開始されることがなくなります。

  3. タイマーの設定:

    // Setup timer, if deadline is given.
    var timer <-chan time.Time
    if delta > 0 { // 変更点: dt > 0 から delta > 0 に変更
        t := time.NewTimer(time.Duration(delta) * time.Nanosecond)
        defer t.Stop()
        timer = t.C
    }
    

    タイマーの設定部分も変更されています。以前はdt(デッドラインまでの残り時間)が1ナノ秒未満の場合でも1ナノ秒としてタイマーを設定していましたが、新しいロジックでは、delta0より大きい(つまり、まだデッドラインまで時間がある)場合にのみタイマーを設定します。これにより、デッドラインが既に過ぎている場合にタイマーを不必要に設定するオーバーヘッドも削減されます。

この変更により、ExecIO関数は、デッドラインが既に過ぎている場合には、実際のI/O操作を試みることなく、迅速にタイムアウトエラーを返すようになりました。これは、特に高頻度でI/O操作が行われる環境や、デッドラインが厳密に管理されるアプリケーションにおいて、パフォーマンスと堅牢性を向上させる重要な改善です。

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

変更はsrc/pkg/net/fd_windows.goファイルに集中しています。

--- a/src/pkg/net/fd_windows.go
+++ b/src/pkg/net/fd_windows.go
@@ -169,6 +169,15 @@ func (s *ioSrv) ProcessRemoteIO() {
 func (s *ioSrv) ExecIO(oi anOpIface, deadline int64) (int, error) {
 	var err error
 	o := oi.Op()
+	// Calculate timeout delta.
+	var delta int64
+	if deadline != 0 {
+		delta = deadline - time.Now().UnixNano()
+		if delta <= 0 {
+			return 0, &OpError{oi.Name(), o.fd.net, o.fd.laddr, errTimeout}
+		}
+	}
+	// Start IO.
 	if canCancelIO {
 		err = oi.Submit()
 	} else {
@@ -188,12 +197,8 @@ func (s *ioSrv) ExecIO(oi anOpIface, deadline int64) (int, error) {
 	}
 	// Setup timer, if deadline is given.
 	var timer <-chan time.Time
-	if deadline != 0 {
-		dt := deadline - time.Now().UnixNano()
-		if dt < 1 {
-			dt = 1
-		}
-		t := time.NewTimer(time.Duration(dt) * time.Nanosecond)
+	if delta > 0 {
+		t := time.NewTimer(time.Duration(delta) * time.Nanosecond)
 		defer t.Stop()
 		timer = t.C
 	}

コアとなるコードの解説

このコミットのコアとなる変更は、ExecIO関数内でI/O操作を開始する前に、デッドラインが有効であるかをチェックするロジックを追加した点です。

  1. デッドラインの事前チェックと早期リターン:

    	// Calculate timeout delta.
    	var delta int64
    	if deadline != 0 {
    		delta = deadline - time.Now().UnixNano()
    		if delta <= 0 {
    			return 0, &OpError{oi.Name(), o.fd.net, o.fd.laddr, errTimeout}
    		}
    	}
    

    このブロックは、ExecIO関数が呼び出された時点で、引数deadlineに設定された時刻が既に過去のものであるか、あるいは現在時刻と同時であるかを判断します。

    • deadline != 0: デッドラインが設定されている場合のみ処理を進めます。
    • delta = deadline - time.Now().UnixNano(): デッドラインまでの残り時間をナノ秒で計算します。
    • if delta <= 0: もしdeltaが0以下であれば、デッドラインは既に過ぎているか、現在時刻と一致しています。この場合、I/O操作を試みる意味がないため、return 0, &OpError{...}によって即座にタイムアウトエラーを返します。これにより、oi.Submit()などの実際のI/O開始処理がスキップされ、無駄なシステムコールやリソース消費が回避されます。
  2. タイマー設定ロジックの簡素化:

    	// Setup timer, if deadline is given.
    	var timer <-chan time.Time
    	if delta > 0 {
    		t := time.NewTimer(time.Duration(delta) * time.Nanosecond)
    		defer t.Stop()
    		timer = t.C
    	}
    

    以前のコードでは、dt(デッドラインまでの残り時間)が1ナノ秒未満の場合でも強制的に1ナノ秒としてタイマーを設定していました。しかし、新しいロジックでは、上記の事前チェックでdelta <= 0の場合は既に早期リターンされているため、このタイマー設定ブロックに到達する時点でdeltaは必ず正の値(delta > 0)であることが保証されます。これにより、タイマー設定の条件がif delta > 0とシンプルになり、デッドラインが過ぎた場合に不必要なタイマーを生成するオーバーヘッドもなくなります。

これらの変更により、GoのnetパッケージはWindows環境において、デッドライン処理をより効率的かつ正確に行うようになり、不要なI/O操作の試行を未然に防ぐことで、全体的なパフォーマンスと安定性が向上しました。

関連リンク

参考にした情報源リンク

  • 特になし(コミット内容とGo言語の標準ライブラリの知識に基づいて解説を生成しました)