[インデックス 11240] ファイルの概要
このコミットは、Go言語のnet
パッケージにおけるタイムアウト処理の根本的な変更を導入しています。具体的には、既存のSetTimeout
、SetReadTimeout
、SetWriteTimeout
といった相対的なタイムアウト設定メソッドを、SetDeadline
、SetReadDeadline
、SetWriteDeadline
という絶対的なデッドライン設定メソッドに置き換えるものです。これにより、ネットワーク操作のタイムアウトの振る舞いがより予測可能で、高レベルのアプリケーションで制御しやすくなります。
コミット
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操作がタイムアウトするというものでした。しかし、これは以下のような問題を引き起こしていました。
- 粒度の問題: タイムアウトが個々の
Read
やWrite
呼び出しに適用されるため、複数の小さなI/O操作で構成される高レベルのプロトコル(例: HTTPリクエストの読み込み)では、全体の処理時間が予期せず長くなる可能性がありました。各I/O操作がタイムアウト時間内に完了しても、次の操作までの間に遅延が生じれば、全体の処理はタイムアウトしませんでした。 - 予測の困難さ: アプリケーション開発者は、特定のネットワーク操作(例: HTTPリクエスト全体)がいつまでに完了すべきかを制御したい場合が多いですが、個々の
Read
/Write
の回数を正確に予測することは困難です。そのため、適切なタイムアウト値を設定することが難しく、アプリケーションの応答性や信頼性に影響を与える可能性がありました。 - 複雑なロジック: 高レベルで全体のタイムアウトを制御するためには、アプリケーション側でタイマーを管理し、各I/O操作の後に残り時間を計算して
SetTimeout
を再設定するなどの複雑なロジックが必要になる場合がありました。
これらの問題を解決するため、Go 1のリリースに向けて、タイムアウトの概念を「相対的な時間」から「絶対的なデッドライン(期限)」へと変更することが決定されました。これにより、アプリケーションは特定の時刻までに操作を完了させるという明確な目標を設定できるようになり、I/O操作の途中で発生する遅延に関わらず、その時刻を過ぎると操作が失敗するようになります。これは、特にHTTPサーバーやクライアントなど、高レベルのプロトコルを扱うアプリケーションにおいて、より堅牢で予測可能なタイムアウト処理を実現するために不可欠な変更でした。
この変更は、GoのIssue #2723 (net: SetTimeout is confusing
) で議論され、その解決策として導入されました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の基本的な概念とネットワークプログラミングの知識が必要です。
-
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
: 書き込み操作のタイムアウトを設定します。
-
タイムアウト (Timeout) とデッドライン (Deadline) の違い:
- タイムアウト (Timeout): ある操作が開始されてから、指定された「期間」が経過すると操作が中断されるという概念です。例えば、「10秒のタイムアウト」とは、操作が開始されてから10秒以内に完了しなければならないことを意味します。従来の
SetTimeout
はこの考え方に基づいていました。問題は、複数の小さな操作が連続する場合、各操作がタイムアウト期間内に完了しても、操作間の遅延によって全体の処理が非常に長くなる可能性がある点です。 - デッドライン (Deadline): ある操作が、指定された「絶対的な時刻」までに完了しなければならないという概念です。例えば、「午後5時までのデッドライン」とは、現在の操作が午後5時までに完了しなければならないことを意味します。この時刻を過ぎると、その後の操作はすぐに失敗します。このコミットで導入された
SetDeadline
はこの考え方に基づいています。これにより、高レベルのアプリケーションは、全体の処理がいつまでに完了すべきかを明確に制御できます。
- タイムアウト (Timeout): ある操作が開始されてから、指定された「期間」が経過すると操作が中断されるという概念です。例えば、「10秒のタイムアウト」とは、操作が開始されてから10秒以内に完了しなければならないことを意味します。従来の
-
time.Time
とtime.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
はこの型を使用します。
-
net.Error
インターフェースとTimeout()
メソッド:net
パッケージで発生するネットワーク関連のエラーは、net.Error
インターフェースを実装することがあります。このインターフェースにはTimeout() bool
メソッドがあり、エラーがタイムアウトによって発生したものであるかどうかを判定するために使用されます。タイムアウトエラーの場合、このメソッドはtrue
を返します。
これらの概念を理解することで、なぜSetTimeout
からSetDeadline
への変更が重要であり、Goのネットワークプログラミングにどのような影響を与えるのかが明確になります。
技術的詳細
このコミットの技術的詳細は、net.Conn
インターフェースの変更と、その下位レイヤーでのタイムアウト処理の実装変更に集約されます。
-
インターフェースの変更: 最も重要な変更は、
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{}
)は、デッドラインを設定しない(タイムアウトを無効にする)ことを意味します。 -
下位レイヤーでの実装変更 (
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_delta
やwdeadline_delta
に基づいて現在の時刻にデルタを加算してデッドラインを計算するのではなく、rdeadline
やwdeadline
に直接設定された絶対時刻(Unixナノ秒)を使用するようになりました。 これにより、I/O操作がブロックされた場合、その操作は設定されたデッドライン時刻まで待機し、デッドラインを過ぎるとタイムアウトエラーを返します。デッドラインは一度設定されると、その接続に対するすべての後続のI/O操作に適用されます。
-
-
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操作がキャンセルされます。 -
影響と利点:
- 予測可能性の向上: タイムアウトが絶対時刻に基づくため、ネットワーク操作全体の完了時間をより正確に制御できるようになりました。例えば、HTTPリクエスト全体がN秒以内に完了することを保証できます。
- 高レベルでの制御: アプリケーション開発者は、個々の
Read
/Write
の回数を気にすることなく、高レベルのプロトコル(例: HTTPサーバーのハンドラ)でデッドラインを設定できます。 - コードの簡素化: アプリケーション側で複雑なタイムアウト管理ロジックを実装する必要がなくなりました。
- 一貫性:
net
パッケージ全体でタイムアウトのセマンティクスが一貫したものになりました。
この変更は、Go 1のリリースにおける重要なAPI変更の一つであり、Goのネットワークスタックの堅牢性と使いやすさを大幅に向上させました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に以下の2つのファイルに集中しています。
src/pkg/net/net.go
:net.Conn
インターフェースの定義が変更されています。これは、Goのネットワーク接続の基本的な振る舞いを規定する最も重要なインターフェースです。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_delta
とwdeadline_delta
フィールドが削除されました。これらは相対的なタイムアウト期間を保持していました。rdeadline
とwdeadline
フィールドは残されましたが、これらが保持する値は相対的な期間ではなく、絶対的なデッドライン時刻(Unixエポックからのナノ秒)を直接格納するようになりました。
- I/O操作メソッド内のタイムアウトロジックの削除:
Read
,ReadFrom
,ReadMsg
,Write
,WriteTo
,WriteMsg
,accept
といった各I/O操作メソッドの冒頭にあった、rdeadline_delta
やwdeadline_delta
に基づいてrdeadline
やwdeadline
を計算するロジックが削除されました。 これは、SetReadDeadline
やSetWriteDeadline
が呼び出された時点で、rdeadline
やwdeadline
に既に絶対的なデッドライン時刻が設定されているため、各I/O操作のたびに再計算する必要がなくなったことを意味します。 実際のタイムアウトチェックは、pollserver
(Goの内部的なI/O多重化メカニズム)によって、rdeadline
やwdeadline
に設定された絶対時刻に基づいて行われます。
これらの変更により、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
パッケージの公式ドキュメント。 - ネットワークプログラミングにおけるタイムアウトとデッドラインの概念に関する一般的な情報。