[インデックス 11242] ファイルの概要
このコミットは、Go言語のネットワークパッケージにおけるWindows固有のファイルディスクリプタ (fd) 処理に関する修正です。具体的には、fd_windows.go
内の ExecIO
関数において、タイマーの生成に time.NewTicker
ではなく time.NewTimer
を使用するように変更しています。これにより、潜在的な非効率性を解消し、コードの意図をより正確に反映させています。
コミット
commit 98af38807e9bc240b83d1a0aa6985a2b4a9f9778
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Jan 18 16:49:59 2012 -0800
net: use NewTimer, not NewTicker, in fd_windows.go
It works with NewTicker too, but is potentially a bit less efficient,
and reads wrong.
This is what happens when you TBR Windows changes, I guess.
R=golang-dev, gri, iant
CC=golang-dev
https://golang.org/cl/5536060
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/98af38807e9bc240b83d1a0aa6985a2b4a9f9778
元コミット内容
net: use NewTimer, not NewTicker, in fd_windows.go
It works with NewTicker too, but is potentially a bit less efficient,
and reads wrong.
This is what happens when you TBR Windows changes, I guess.
R=golang-dev, gri, iant
CC=golang-dev
https://golang.org/cl/5536060
変更の背景
この変更の背景には、Go言語のネットワークパッケージがWindows環境でI/O操作を処理する際の効率性と正確性の向上が挙げられます。元のコードでは、特定のタイムアウト処理に time.NewTicker
が使用されていましたが、これは単発のイベントではなく定期的なイベントを生成するために設計されたものです。コミットメッセージにある「potentially a bit less efficient, and reads wrong」という記述は、NewTicker
の誤用がパフォーマンスの低下やコードの意図の不明瞭さにつながっていたことを示唆しています。
特に、WindowsにおけるネットワークI/Oは、Unix系システムとは異なる非同期I/Oモデル(I/O完了ポートなど)を使用することが多く、タイムアウト処理もそれに合わせて最適化される必要があります。NewTicker
は定期的なイベントを生成し続けるため、単一のタイムアウトイベントを待つ場合には不要なリソース消費や処理オーバーヘッドが発生する可能性があります。このコミットは、このような非効率性を解消し、コードの意図を明確にするために time.NewTimer
への切り替えを行っています。
また、「This is what happens when you TBR Windows changes, I guess.」というコメントは、"To Be Reviewed" (TBR) されたWindows関連の変更が、レビュープロセス中に見落とされたり、十分に考慮されなかったりした結果、このような非効率なコードが混入した可能性を示唆しています。これは、クロスプラットフォーム開発におけるレビューの難しさや、特定のOS固有の挙動に対する深い理解の重要性を浮き彫りにしています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とWindowsのI/Oモデルに関する基本的な知識が必要です。
Go言語の time
パッケージ
Go言語の time
パッケージは、時間に関する操作(時刻の取得、期間の計算、タイマーやティッカーの作成など)を提供します。
-
time.NewTimer(d Duration) *Timer
:NewTimer
は、指定された期間d
が経過した後に単一のイベントを発生させるタイマーを作成します。イベントはTimer
型のC
チャネルに送信されます。一度イベントが発生すると、タイマーは停止します。再利用するにはReset
メソッドを呼び出す必要があります。これは、特定の操作のタイムアウトを実装する際など、一度だけ発生するイベントを待つ場合に最適です。 -
time.NewTicker(d Duration) *Ticker
:NewTicker
は、指定された期間d
ごとに定期的にイベントを発生させるティッカーを作成します。イベントはTicker
型のC
チャネルに送信されます。ティッカーはStop
メソッドが呼び出されるまでイベントを生成し続けます。これは、定期的な処理(例: ログのフラッシュ、統計情報の収集)を実装する際に使用されます。
このコミットの文脈では、ExecIO
関数が単一のI/O操作の完了を待つためのタイムアウト処理を実装しているため、定期的なイベントを生成する NewTicker
よりも、単一のイベントを生成する NewTimer
の方が適切です。
Windowsにおける非同期I/OとI/O完了ポート (IOCP)
Windowsでは、高性能な非同期I/Oを実現するためにI/O完了ポート (I/O Completion Ports: IOCP) が広く利用されます。アプリケーションはI/O操作を開始し、その完了を待つ間、他の処理を続行できます。I/O操作が完了すると、システムは完了イベントをIOCPにキューイングし、アプリケーションはそこからイベントを取得して処理します。
Go言語のネットワークパッケージは、Windows上でこのIOCPモデルを抽象化し、Goのgoroutineとチャネルの並行性モデルに適合させるための内部的なメカニズムを持っています。fd_windows.go
は、この抽象化レイヤーの一部であり、Windows固有のファイルディスクリプタ(ソケットなど)に対するI/O操作を管理します。
ExecIO
関数は、おそらく特定のI/O操作(読み取り、書き込みなど)を実行し、その完了を待つためのメカニズムを提供しています。deadline
パラメータは、操作が完了するまでの最大時間を指定し、この時間を超えた場合にタイムアウトとして処理されます。
技術的詳細
このコミットの技術的な詳細は、time.NewTimer
と time.NewTicker
のセマンティクスの違い、およびそれが ExecIO
関数のタイムアウト処理に与える影響に集約されます。
ExecIO
関数は、oi anOpIface
というインターフェースを通じてI/O操作を実行し、deadline
に基づいてタイムアウトを処理します。この関数は、単一のI/O操作が完了するか、または指定されたデッドラインに達するかのいずれかを待機します。
元のコードでは、time.NewTicker(time.Duration(dt) * time.Nanosecond)
を使用していました。ここで dt
はデッドラインまでの残り時間をナノ秒単位で表しています。NewTicker
は、dt
ナノ秒ごとにイベントを ticker.C
チャネルに送信します。しかし、ExecIO
関数が必要としているのは、dt
時間が経過した 後 に一度だけ発生するタイムアウトイベントです。
select
ステートメント内で <-ticker.C
を使用することで、確かに最初の dt
時間が経過した後にタイムアウトイベントを検出できます。しかし、NewTicker
はその後も dt
ごとにイベントを生成し続けます。これは、ExecIO
関数がタイムアウトを処理して終了した後も、不要なgoroutineとチャネルイベントが生成され続ける可能性を意味します。defer ticker.Stop()
があるため、関数が終了する際にはティッカーは停止されますが、その間にも不要なイベントが生成される可能性があり、これはリソースの無駄遣いや、わずかながらもパフォーマンスのオーバーヘッドにつながります。
対照的に、time.NewTimer(time.Duration(dt) * time.Nanosecond)
は、dt
時間が経過した後に一度だけイベントを timer.C
チャネルに送信します。イベントが送信されると、タイマーは自動的に停止します(または、Reset
が呼び出されない限り、それ以上イベントを生成しません)。これは、単一のタイムアウトイベントを待つという ExecIO
の要件に完全に合致しています。
したがって、この変更は以下の点で改善をもたらします。
- 効率性:
NewTicker
が生成する不要な定期イベントを排除し、リソースの消費を抑えます。 - 正確性/意図の明確化: コードの意図(単一のタイムアウトを待つ)が
NewTimer
の使用によってより明確になります。NewTicker
の使用は、コードを読む人に対して「なぜ定期的なイベントが必要なのか?」という誤解を与える可能性があります。 - リソース管理:
NewTimer
は一度イベントを発生させるとそれ以上イベントを生成しないため、NewTicker
のように明示的にStop()
を呼び出す必要性が(この特定のユースケースでは)低くなりますが、defer timer.Stop()
を残すことで、タイマーが不要になった際に確実にリソースが解放されるようにしています。これは良いプラクティスです。
コミットメッセージの「reads wrong」という表現は、コードの意図が NewTicker
の使用によって誤って伝わっていたことを指していると考えられます。
コアとなるコードの変更箇所
変更は src/pkg/net/fd_windows.go
ファイルの ExecIO
関数内で行われています。
--- a/src/pkg/net/fd_windows.go
+++ b/src/pkg/net/fd_windows.go
@@ -179,11 +179,11 @@ func (s *ioSrv) ExecIO(oi anOpIface, deadline int64) (n int, err error) {
if dt < 1 {
dt = 1
}
- ticker := time.NewTicker(time.Duration(dt) * time.Nanosecond)
- defer ticker.Stop()
+ timer := time.NewTimer(time.Duration(dt) * time.Nanosecond)
+ defer timer.Stop()
select {\
case r = <-o.resultc:
- case <-ticker.C:
+ case <-timer.C:
s.canchan <- oi
<-o.errnoc
r = <-o.resultc
具体的には、以下の3行が変更されています。
ticker := time.NewTicker(...)
がtimer := time.NewTimer(...)
に変更。defer ticker.Stop()
がdefer timer.Stop()
に変更。case <-ticker.C:
がcase <-timer.C:
に変更。
コアとなるコードの解説
ExecIO
関数は、Windows環境での非同期I/O操作の実行とタイムアウト処理を担当しています。
func (s *ioSrv) ExecIO(oi anOpIface, deadline int64) (n int, err error) {
// ... (前略) ...
// dt はデッドラインまでの残り時間(ナノ秒)
if dt < 1 {
dt = 1 // 少なくとも1ナノ秒は待つ
}
// 変更前: 定期的にイベントを発生させるティッカーを作成
// ticker := time.NewTicker(time.Duration(dt) * time.Nanosecond)
// defer ticker.Stop() // 関数終了時にティッカーを停止
// 変更後: 一度だけイベントを発生させるタイマーを作成
timer := time.NewTimer(time.Duration(dt) * time.Nanosecond)
defer timer.Stop() // 関数終了時にタイマーを停止 (良い習慣)
select {
case r = <-o.resultc: // I/O操作の結果を待つ
// I/O操作が完了した場合
// 変更前: ティッカーのチャネルからイベントを待つ
// case <-ticker.C: // タイムアウトイベントを待つ
// 変更後: タイマーのチャネルからイベントを待つ
case <-timer.C: // タイムアウトイベントを待つ
// タイムアウトした場合の処理
s.canchan <- oi // I/O操作をキャンセルするシグナルを送る
<-o.errnoc // エラーチャネルから結果を待つ(キャンセル処理の完了を待つためか)
r = <-o.resultc // キャンセル後の最終結果を取得
}
// ... (後略) ...
}
この select
ステートメントは、Goの並行処理における重要なパターンです。複数の通信操作(チャネルからの受信)を同時に待機し、いずれかの操作が準備できた時点でその操作を実行します。
case r = <-o.resultc:
: これは、実際のI/O操作が完了し、その結果がo.resultc
チャネルに送信されるのを待っています。I/O操作が成功または失敗して結果が返されれば、このケースが選択されます。case <-timer.C:
(変更後): これは、dt
時間が経過し、timer
がタイムアウトイベントをtimer.C
チャネルに送信するのを待っています。もしI/O操作がdt
時間内に完了しなかった場合、このケースが選択され、タイムアウト処理が実行されます。
この変更により、ExecIO
関数は、単一のI/O操作の完了または単一のタイムアウトイベントの発生という、その本来の目的に合致した効率的かつ正確な方法でタイムアウトを処理するようになりました。NewTicker
の代わりに NewTimer
を使用することで、不要な定期的なイベント生成が回避され、リソースの無駄遣いがなくなります。
関連リンク
- Go言語の
time
パッケージドキュメント: https://pkg.go.dev/time - Go言語の
time.NewTimer
ドキュメント: https://pkg.go.dev/time#NewTimer - Go言語の
time.NewTicker
ドキュメント: https://pkg.go.dev/time#NewTicker - Go言語の
select
ステートメントに関する公式ドキュメント: https://go.dev/tour/concurrency/5 - Go言語の変更セット (Gerrit): https://golang.org/cl/5536060
参考にした情報源リンク
- Go言語公式ドキュメント
- Go言語のソースコード (特に
src/pkg/net/fd_windows.go
) time.NewTimer
とtime.NewTicker
の違いに関する一般的なGoプログラミングの議論- Windows I/O完了ポート (IOCP) に関する一般的な情報 (例: Microsoft Learn ドキュメント)
- GitHubのコミット履歴と関連する議論