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

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

このコミットは、Go言語のnetパッケージにおけるタイムアウト処理の根本的な変更を導入しています。具体的には、既存のSetTimeoutSetReadTimeoutSetWriteTimeoutといった相対的なタイムアウト設定メソッドを、SetDeadlineSetReadDeadlineSetWriteDeadlineという絶対的なデッドライン設定メソッドに置き換えるものです。これにより、ネットワーク操作のタイムアウトの振る舞いがより予測可能で、高レベルのアプリケーションで制御しやすくなります。

コミット

commit b71883e9b0eff7e89081d20204bf33f369cdf735
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Jan 18 16:24:06 2012 -0800

    net: change SetTimeout to SetDeadline
    
    Previously, a timeout (in int64 nanoseconds) applied to a granularity
    even smaller than one operation:  a 100 byte read with a 1 second timeout
    could take 100 seconds, if the bytes all arrived on the network 1 second
    apart.  This was confusing.
    
    Rather than making the timeout granularity be per-Read/Write,
    this CL makes callers set an absolute deadline (in time.Time)
    after which operations will fail.  This makes it possible to
    set deadlines at higher levels, without knowing exactly how
    many read/write operations will happen in e.g. reading an HTTP
    request.
    
    Fixes #2723
    
    R=r, rsc, dave
    CC=golang-dev
    https://golang.org/cl/5555048

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

https://github.com/golang/go/commit/b71883e9b0eff7e89081d20204bf33f369cdf735

元コミット内容

このコミットの元々の問題意識は、netパッケージのSetTimeout系のメソッドが提供するタイムアウトの粒度が、ユーザーの期待と異なる振る舞いをしていた点にあります。コミットメッセージに具体例として挙げられているのは、「1秒のタイムアウトを設定した100バイトの読み込みが、各バイトが1秒間隔でネットワークに到着した場合、合計で100秒かかってしまう」という状況です。これは、タイムアウトが個々のRead/Write操作ごとに適用されるため、全体としての操作完了までの時間が非常に長くなる可能性を示していました。この振る舞いは「混乱を招く (confusing)」と表現されており、開発者が意図するタイムアウトとはかけ離れた結果を生むことが問題視されていました。

変更の背景

この変更の背景には、Go言語のネットワークプログラミングにおけるタイムアウトのセマンティクスを改善し、より直感的で強力な制御を可能にするという目的があります。

従来のSetTimeout系のメソッドは、引数としてナノ秒単位の相対的な時間を受け取り、その時間が経過すると現在のI/O操作がタイムアウトするというものでした。しかし、これは以下のような問題を引き起こしていました。

  1. 粒度の問題: タイムアウトが個々のReadWrite呼び出しに適用されるため、複数の小さなI/O操作で構成される高レベルのプロトコル(例: HTTPリクエストの読み込み)では、全体の処理時間が予期せず長くなる可能性がありました。各I/O操作がタイムアウト時間内に完了しても、次の操作までの間に遅延が生じれば、全体の処理はタイムアウトしませんでした。
  2. 予測の困難さ: アプリケーション開発者は、特定のネットワーク操作(例: HTTPリクエスト全体)がいつまでに完了すべきかを制御したい場合が多いですが、個々のRead/Writeの回数を正確に予測することは困難です。そのため、適切なタイムアウト値を設定することが難しく、アプリケーションの応答性や信頼性に影響を与える可能性がありました。
  3. 複雑なロジック: 高レベルで全体のタイムアウトを制御するためには、アプリケーション側でタイマーを管理し、各I/O操作の後に残り時間を計算してSetTimeoutを再設定するなどの複雑なロジックが必要になる場合がありました。

これらの問題を解決するため、Go 1のリリースに向けて、タイムアウトの概念を「相対的な時間」から「絶対的なデッドライン(期限)」へと変更することが決定されました。これにより、アプリケーションは特定の時刻までに操作を完了させるという明確な目標を設定できるようになり、I/O操作の途中で発生する遅延に関わらず、その時刻を過ぎると操作が失敗するようになります。これは、特にHTTPサーバーやクライアントなど、高レベルのプロトコルを扱うアプリケーションにおいて、より堅牢で予測可能なタイムアウト処理を実現するために不可欠な変更でした。

この変更は、GoのIssue #2723 (net: SetTimeout is confusing) で議論され、その解決策として導入されました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の基本的な概念とネットワークプログラミングの知識が必要です。

  1. net.Conn インターフェース: Go言語のnetパッケージにおけるConnインターフェースは、ネットワーク接続の汎用的な抽象化を提供します。TCP接続、UDP接続、Unixドメインソケットなど、様々な種類のネットワーク接続がこのインターフェースを実装します。 主要なメソッドには以下のようなものがあります。

    • Read(b []byte) (n int, err error): 接続からデータを読み込みます。
    • Write(b []byte) (n int, err error): 接続にデータを書き込みます。
    • LocalAddr() net.Addr: ローカルネットワークアドレスを返します。
    • RemoteAddr() net.Addr: リモートネットワークアドレスを返します。
    • (変更前)SetTimeout(nsec int64) error: 読み書き両方のタイムアウトを設定します。
    • (変更前)SetReadTimeout(nsec int64) error: 読み込み操作のタイムアウトを設定します。
    • (変更前)SetWriteTimeout(nsec int64) error: 書き込み操作のタイムアウトを設定します。
  2. タイムアウト (Timeout) とデッドライン (Deadline) の違い:

    • タイムアウト (Timeout): ある操作が開始されてから、指定された「期間」が経過すると操作が中断されるという概念です。例えば、「10秒のタイムアウト」とは、操作が開始されてから10秒以内に完了しなければならないことを意味します。従来のSetTimeoutはこの考え方に基づいていました。問題は、複数の小さな操作が連続する場合、各操作がタイムアウト期間内に完了しても、操作間の遅延によって全体の処理が非常に長くなる可能性がある点です。
    • デッドライン (Deadline): ある操作が、指定された「絶対的な時刻」までに完了しなければならないという概念です。例えば、「午後5時までのデッドライン」とは、現在の操作が午後5時までに完了しなければならないことを意味します。この時刻を過ぎると、その後の操作はすぐに失敗します。このコミットで導入されたSetDeadlineはこの考え方に基づいています。これにより、高レベルのアプリケーションは、全体の処理がいつまでに完了すべきかを明確に制御できます。
  3. time.Timetime.Duration: Go言語のtimeパッケージは、時間に関する型と関数を提供します。

    • time.Duration: 期間を表す型です。ナノ秒単位で内部的に表現されます。例えば、10 * time.Secondは10秒の期間を表します。従来のSetTimeoutはこの型(またはint64ナノ秒)を使用していました。
    • time.Time: 特定の時点(絶対時刻)を表す型です。time.Now()で現在の時刻を取得できます。time.Time型の値にtime.Durationを加算することで、未来の時刻を計算できます(例: time.Now().Add(10 * time.Second))。新しいSetDeadlineはこの型を使用します。
  4. net.Error インターフェースと Timeout() メソッド: netパッケージで発生するネットワーク関連のエラーは、net.Errorインターフェースを実装することがあります。このインターフェースにはTimeout() boolメソッドがあり、エラーがタイムアウトによって発生したものであるかどうかを判定するために使用されます。タイムアウトエラーの場合、このメソッドはtrueを返します。

これらの概念を理解することで、なぜSetTimeoutからSetDeadlineへの変更が重要であり、Goのネットワークプログラミングにどのような影響を与えるのかが明確になります。

技術的詳細

このコミットの技術的詳細は、net.Connインターフェースの変更と、その下位レイヤーでのタイムアウト処理の実装変更に集約されます。

  1. インターフェースの変更: 最も重要な変更は、net.Connインターフェースの定義です。

    • SetTimeout(nsec int64) error
    • SetReadTimeout(nsec int64) error
    • SetWriteTimeout(nsec int64) error これらのメソッドが削除され、代わりに以下のメソッドが追加されました。
    • SetDeadline(t time.Time) error
    • SetReadDeadline(t time.Time) error
    • SetWriteDeadline(t time.Time) error

    これにより、タイムアウトの指定方法が相対時間(int64ナノ秒)から絶対時刻(time.Time)に変わりました。time.Timeのゼロ値(time.Time{})は、デッドラインを設定しない(タイムアウトを無効にする)ことを意味します。

  2. 下位レイヤーでの実装変更 (net/fd.go, net/fd_windows.go, net/sockopt.go): net.Connインターフェースの実装は、内部的にnetFD構造体を通じて行われます。この構造体は、ファイルディスクリプタ(Unix系)やソケットハンドル(Windows)をラップし、実際のI/O操作を処理します。

    • netFD構造体の変更: 従来のrdeadline_delta (読み込みタイムアウト期間) と wdeadline_delta (書き込みタイムアウト期間) フィールドが削除され、代わりに rdeadline (読み込みデッドライン) と wdeadline (書き込みデッドライン) フィールドが追加されました。これらのフィールドは、Unixエポックからのナノ秒数をint64で保持します。

      // 変更前
      // rdeadline_delta int64
      // rdeadline       int64
      // wdeadline_delta int64
      // wdeadline       int64
      
      // 変更後
      rdeadline int64
      wdeadline int64
      
    • setReadTimeout/setWriteTimeout/setTimeout から setReadDeadline/setWriteDeadline/setDeadline への変更: net/sockopt.goでは、これらのヘルパー関数が更新されました。

      // 変更前
      // func setReadTimeout(fd *netFD, nsec int64) error { fd.rdeadline_delta = nsec; return nil }
      // func setWriteTimeout(fd *netFD, nsec int64) error { fd.wdeadline_delta = nsec; return nil }
      // func setTimeout(fd *netFD, nsec int64) error { ... }
      
      // 変更後
      func setReadDeadline(fd *netFD, t time.Time) error { fd.rdeadline = t.UnixNano(); return nil }
      func setWriteDeadline(fd *netFD, t time.Time) error { fd.wdeadline = t.UnixNano(); return nil }
      func setDeadline(fd *netFD, t time.Time) error { ... }
      

      これにより、netFDのデッドラインフィールドに直接time.TimeのUnixナノ秒表現が設定されるようになりました。

    • I/O操作 (Read, Write, Acceptなど) の変更: net/fd.goおよびnet/fd_windows.go内のRead, Write, ReadFrom, WriteTo, AcceptなどのI/O操作メソッドでは、タイムアウトの計算ロジックが変更されました。 従来のrdeadline_deltawdeadline_deltaに基づいて現在の時刻にデルタを加算してデッドラインを計算するのではなく、rdeadlinewdeadlineに直接設定された絶対時刻(Unixナノ秒)を使用するようになりました。 これにより、I/O操作がブロックされた場合、その操作は設定されたデッドライン時刻まで待機し、デッドラインを過ぎるとタイムアウトエラーを返します。デッドラインは一度設定されると、その接続に対するすべての後続のI/O操作に適用されます。

  3. Windows固有のI/O処理 (net/fd_windows.go): Windowsでは、非同期I/O (overlapped I/O) とI/O完了ポート (IOCP) を使用してタイムアウトを処理します。このコミットでは、ioSrv.ExecIO関数がdeadline_deltaではなくdeadline(Unixナノ秒)を受け取るように変更されました。 タイムアウトの監視には、time.NewTickerが使用され、デッドラインまでの残り時間を計算してタイマーを設定します。デッドラインに達すると、CancelIOが呼び出されてI/O操作がキャンセルされます。

  4. 影響と利点:

    • 予測可能性の向上: タイムアウトが絶対時刻に基づくため、ネットワーク操作全体の完了時間をより正確に制御できるようになりました。例えば、HTTPリクエスト全体がN秒以内に完了することを保証できます。
    • 高レベルでの制御: アプリケーション開発者は、個々のRead/Writeの回数を気にすることなく、高レベルのプロトコル(例: HTTPサーバーのハンドラ)でデッドラインを設定できます。
    • コードの簡素化: アプリケーション側で複雑なタイムアウト管理ロジックを実装する必要がなくなりました。
    • 一貫性: netパッケージ全体でタイムアウトのセマンティクスが一貫したものになりました。

この変更は、Go 1のリリースにおける重要なAPI変更の一つであり、Goのネットワークスタックの堅牢性と使いやすさを大幅に向上させました。

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

このコミットにおけるコアとなるコードの変更箇所は、主に以下の2つのファイルに集中しています。

  1. src/pkg/net/net.go: net.Connインターフェースの定義が変更されています。これは、Goのネットワーク接続の基本的な振る舞いを規定する最も重要なインターフェースです。
  2. src/pkg/net/fd.go: netFD構造体と、そのI/O操作(Read, Write, Acceptなど)の実装が変更されています。このファイルは、Goのネットワークスタックの低レベルな部分を扱い、実際のシステムコールとタイムアウト処理を管理します。

これらの変更は、SetTimeoutからSetDeadlineへの移行の核心部分を形成しています。

コアとなるコードの解説

src/pkg/net/net.go の変更

このファイルでは、Connインターフェースの定義が以下のように変更されました。

--- a/src/pkg/net/net.go
+++ b/src/pkg/net/net.go
@@ -9,7 +9,10 @@ package net
 // TODO(rsc):
 //	support for raw ethernet sockets
 
-import "errors"
+import (
+	"errors"
+	"time"
+)
 
 // Addr represents a network end point address.
 type Addr interface {
@@ -38,21 +41,23 @@ type Conn interface {
 	// RemoteAddr returns the remote network address.
 	RemoteAddr() Addr
 
-	// SetTimeout sets the read and write deadlines associated
+	// SetDeadline sets the read and write deadlines associated
 	// with the connection.
-	SetTimeout(nsec int64) error
-
-	// SetReadTimeout sets the time (in nanoseconds) that
-	// Read will wait for data before returning an error with Timeout() == true.
-	// Setting nsec == 0 (the default) disables the deadline.
-	SetReadTimeout(nsec int64) error
-
-	// SetWriteTimeout sets the time (in nanoseconds) that
-	// Write will wait to send its data before returning an error with Timeout() == true.
-	// Setting nsec == 0 (the default) disables the deadline.
+	SetDeadline(t time.Time) error
+
+	// SetReadDeadline sets the deadline for all Read calls to return.
+	// If the deadline is reached, Read will fail with a timeout
+	// (see type Error) instead of blocking.
+	// A zero value for t means Read will not time out.
+	SetReadDeadline(t time.Time) error
+
+	// SetWriteDeadline sets the deadline for all Write calls to return.
+	// If the deadline is reached, Write will fail with a timeout
+	// (see type Error) instead of blocking.
+	// A zero value for t means Write will not time out.
 	// Even if write times out, it may return n > 0, indicating that
 	// some of the data was successfully written.
-	SetWriteTimeout(nsec int64) error
+	SetWriteDeadline(t time.Time) error
 }

解説:

  • import "time" が追加され、time.Time型を使用できるようになりました。
  • 従来のSetTimeout, SetReadTimeout, SetWriteTimeoutメソッドが削除されました。これらのメソッドはint64型のnsec(ナノ秒)を引数として受け取り、相対的なタイムアウトを設定していました。
  • 新たにSetDeadline, SetReadDeadline, SetWriteDeadlineメソッドが追加されました。これらのメソッドはtime.Time型のtを引数として受け取り、絶対的なデッドラインを設定します。
  • コメントも更新され、新しいメソッドのセマンティクス(ゼロ値のtime.Timeがタイムアウトを無効にすること、タイムアウト時にnet.Errorが返されることなど)が明確に記述されています。

この変更は、net.Connインターフェースを使用するすべてのコードに影響を与え、タイムアウトの指定方法を統一し、より強力な制御を可能にしました。

src/pkg/net/fd.go の変更

このファイルでは、netFD構造体の内部フィールドと、I/O操作のタイムアウト処理ロジックが変更されました。

--- a/src/pkg/net/fd.go
+++ b/src/pkg/net/fd.go
@@ -33,12 +33,10 @@ type netFD struct {
 	raddr   Addr
 
 	// owned by client
-	rdeadline_delta int64
-	rdeadline       int64
-	rio             sync.Mutex
-	wdeadline_delta int64
-	wdeadline       int64
-	wio             sync.Mutex
+	rdeadline int64
+	rio       sync.Mutex
+	wdeadline int64
+	wio       sync.Mutex
 
 	// owned by fd wait server
 	ncr, ncw int
@@ -388,11 +386,6 @@ func (fd *netFD) Read(p []byte) (n int, err error) {
 	if fd.sysfile == nil {
 		return 0, os.EINVAL
 	}
-	if fd.rdeadline_delta > 0 {
-		fd.rdeadline = pollserver.Now() + fd.rdeadline_delta
-	} else {
-		fd.rdeadline = 0
-	}
 	for {
 		n, err = syscall.Read(fd.sysfile.Fd(), p)
 		if err == syscall.EAGAIN {
@@ -423,11 +416,6 @@ func (fd *netFD) ReadFrom(p []byte) (n int, sa syscall.Sockaddr, err error) {
 	defer fd.rio.Unlock()
 	fd.incref()
 	defer fd.decref()
-	if fd.rdeadline_delta > 0 {
-		fd.rdeadline = pollserver.Now() + fd.rdeadline_delta
-	} else {
-		fd.rdeadline = 0
-	}
 	for {
 		n, sa, err = syscall.Recvfrom(fd.sysfd, p, 0)
 		if err == syscall.EAGAIN {
@@ -456,11 +444,6 @@ func (fd *netFD) ReadMsg(p []byte, oob []byte) (n, oobn, flags int, sa syscall.S
 	defer fd.rio.Unlock()
 	fd.incref()
 	defer fd.decref()
-	if fd.rdeadline_delta > 0 {
-		fd.rdeadline = pollserver.Now() + fd.rdeadline_delta
-	} else {
-		fd.rdeadline = 0
-	}
 	for {
 		n, oobn, flags, sa, err = syscall.Recvmsg(fd.sysfd, p, oob, 0)
 		if err == syscall.EAGAIN {
@@ -493,11 +476,6 @@ func (fd *netFD) Write(p []byte) (n int, err error) {
 	if fd.sysfile == nil {
 		return 0, os.EINVAL
 	}
-	if fd.wdeadline_delta > 0 {
-		fd.wdeadline = pollserver.Now() + fd.wdeadline_delta
-	} else {
-		fd.wdeadline = 0
-	}
 	nn := 0
 
 	for {
@@ -539,11 +517,6 @@ func (fd *netFD) WriteTo(p []byte, sa syscall.Sockaddr) (n int, err error) {
 	defer fd.wio.Unlock()
 	fd.incref()
 	defer fd.decref()
-	if fd.wdeadline_delta > 0 {
-		fd.wdeadline = pollserver.Now() + fd.wdeadline_delta
-	} else {
-		fd.wdeadline = 0
-	}
 	for {
 		err = syscall.Sendto(fd.sysfd, p, 0, sa)
 		if err == syscall.EAGAIN {
@@ -571,11 +534,6 @@ func (fd *netFD) WriteMsg(p []byte, oob []byte, sa syscall.Sockaddr) (n int, oob
 	defer fd.wio.Unlock()
 	fd.incref()
 	defer fd.decref()
-	if fd.wdeadline_delta > 0 {
-		fd.wdeadline = pollserver.Now() + fd.wdeadline_delta
-	} else {
-		fd.wdeadline = 0
-	}
 	for {
 		err = syscall.Sendmsg(fd.sysfd, p, oob, sa, 0)
 		if err == syscall.EAGAIN {
@@ -603,11 +571,6 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (nfd *netFD, err err
 
 	fd.incref()\n 	defer fd.decref()\n-	if fd.rdeadline_delta > 0 {\n-		fd.rdeadline = pollserver.Now() + fd.rdeadline_delta\n-	} else {\n-		fd.rdeadline = 0\n-	}\n \n 	// See ../syscall/exec.go for description of ForkLock.\n 	// It is okay to hold the lock across syscall.Accept

解説:

  • netFD構造体の変更:
    • rdeadline_deltawdeadline_deltaフィールドが削除されました。これらは相対的なタイムアウト期間を保持していました。
    • rdeadlinewdeadlineフィールドは残されましたが、これらが保持する値は相対的な期間ではなく、絶対的なデッドライン時刻(Unixエポックからのナノ秒)を直接格納するようになりました。
  • I/O操作メソッド内のタイムアウトロジックの削除: Read, ReadFrom, ReadMsg, Write, WriteTo, WriteMsg, acceptといった各I/O操作メソッドの冒頭にあった、rdeadline_deltawdeadline_deltaに基づいてrdeadlinewdeadlineを計算するロジックが削除されました。 これは、SetReadDeadlineSetWriteDeadlineが呼び出された時点で、rdeadlinewdeadlineに既に絶対的なデッドライン時刻が設定されているため、各I/O操作のたびに再計算する必要がなくなったことを意味します。 実際のタイムアウトチェックは、pollserver(Goの内部的なI/O多重化メカニズム)によって、rdeadlinewdeadlineに設定された絶対時刻に基づいて行われます。

これらの変更により、Goのネットワークスタックは、個々のI/O操作ごとにタイムアウトを計算するのではなく、接続全体に設定された絶対的なデッドラインに基づいてタイムアウトを処理するようになりました。これにより、前述の「100バイトの読み込みが100秒かかる」といった問題が解消され、より予測可能で堅牢なタイムアウト処理が実現されました。

関連リンク

  • Go Issue #2723: net: SetTimeout is confusing - このコミットが解決した元の問題に関する議論。
  • Go CL 5555048: このコミットに対応するGoの変更リスト(Change List)。
  • Go 1 Release Notes - The net package: Go 1のリリースノートにおけるnetパッケージの変更点に関する記述。このコミットの内容が公式に説明されています。

参考にした情報源リンク

  • 上記の「関連リンク」セクションに記載されたGoの公式ドキュメントとIssue、CL。
  • Go言語のnetパッケージおよびtimeパッケージの公式ドキュメント。
  • ネットワークプログラミングにおけるタイムアウトとデッドラインの概念に関する一般的な情報。