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

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

このコミットは、Go言語の net パッケージにおける accept および connect 操作のデッドライン(期限)処理を修正し、これらの操作がブロックせずに実行できる場合でもデッドラインが尊重されるように変更します。これにより、read および write 操作との一貫性が確保されます。また、デッドラインチェックを pollServer.PrepareRead/Write に分離し、エッジトリガー型 pollServer の準備を進めるとともに、connect/accept 周りに rio/wio ロックを追加することで、pollServer.WaitRead/Write が並行して呼び出されないようにしています。

コミット

commit 0f136f2c057459999f93da2d588325e192160b39
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Mar 7 17:03:40 2013 +0400

    net: fix accept/connect deadline handling
    Ensure that accept/connect respect deadline,
    even if the operation can be executed w/o blocking.
    Note this changes external behavior, but it makes
    it consistent with read/write.
    Factor out deadline check into pollServer.PrepareRead/Write,
    in preparation for edge triggered pollServer.
    Ensure that pollServer.WaitRead/Write are not called concurrently
    by adding rio/wio locks around connect/accept.
    
    R=golang-dev, mikioh.mikioh, bradfitz, iant
    CC=golang-dev
    https://golang.org/cl/7436048

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

https://github.com/golang/go/commit/0f136f2c057459999f93da2d588325e192160b39

元コミット内容

net: fix accept/connect deadline handling accept および connect がデッドラインを尊重するように修正します。 操作がブロックせずに実行できる場合でも、デッドラインが尊重されるようにします。 これは外部の振る舞いを変更しますが、read/write との一貫性を保ちます。 デッドラインチェックを pollServer.PrepareRead/Write に分離し、エッジトリガー型 pollServer の準備を進めます。 connect/accept 周りに rio/wio ロックを追加することで、pollServer.WaitRead/Write が並行して呼び出されないようにします。

変更の背景

Goの net パッケージでは、ネットワークI/O操作に対してデッドライン(タイムアウト)を設定する機能が提供されています。しかし、このコミット以前は、readwrite 操作とは異なり、accept(接続の受け入れ)や connect(接続の確立)操作において、操作が即座に完了し、ブロックが発生しない場合にデッドラインが適切に適用されないという不整合がありました。

具体的には、デッドラインが過去に設定されていたとしても、接続がすぐに利用可能であったり、接続が即座に確立できる状況では、デッドラインを無視して操作が成功してしまう可能性がありました。これは、ユーザーが設定したデッドラインの意図に反する動作であり、アプリケーションが予期せぬ挙動を示す原因となり得ました。

このコミットの目的は、この不整合を解消し、accept および connect 操作も read/write と同様に、操作のブロックの有無にかかわらずデッドラインを厳密に尊重するようにすることです。これにより、GoのネットワークI/Oのデッドライン処理全体の一貫性と予測可能性が向上します。

また、pollServer の内部構造を改善し、将来的なエッジトリガー型ポーリングメカニズムへの移行を容易にするための準備も含まれています。これは、I/Oイベントの処理効率を向上させるための基盤作りです。さらに、connectaccept のような操作中に pollServer.WaitRead/Write が並行して呼び出されることによる潜在的な競合状態を防ぐために、適切なロック機構を導入する必要がありました。

前提知識の解説

  • Goの net パッケージ: Go言語の標準ライブラリの一部で、TCP/IP、UDP、UnixドメインソケットなどのネットワークI/O機能を提供します。ソケットの作成、接続、データの送受信、リスニングなどの基本的なネットワーク操作を抽象化しています。
  • デッドライン (Deadline): ネットワークI/O操作が完了するまでの最大時間を設定するメカニズムです。指定された時間内に操作が完了しない場合、操作はタイムアウトエラーを返します。Goの net.Conn インターフェースには SetReadDeadline, SetWriteDeadline, SetDeadline メソッドがあります。
  • pollServer: Goの net パッケージの内部コンポーネントで、OSのI/O多重化メカニズム(Linuxのepoll、macOSのkqueueなど)を抽象化し、ノンブロッキングI/Oを効率的に処理するためのものです。Goのgoroutineスケジューラと連携し、I/O操作がブロックする際にgoroutineを一時停止させ、I/Oが準備できたときに再開させます。
  • netFD: net パッケージ内でファイルディスクリプタ(ソケット)をラップする構造体です。I/O操作の状態、デッドライン、pollServer への参照などを保持します。
  • syscall.EINPROGRESS / syscall.EAGAIN:
    • EINPROGRESS: ノンブロッキングソケットで connect を呼び出した際に、接続が即座に確立されず、バックグラウンドで進行中であることを示すエラーコードです。
    • EAGAIN (または EWOULDBLOCK): ノンブロッキングソケットで readwrite などのI/O操作を呼び出した際に、データがすぐに利用できない、またはバッファが満杯で書き込めないことを示すエラーコードです。これらのエラーは、操作がブロックする可能性があることを示唆しており、通常はポーリングメカニズム(pollServer)と組み合わせて使用されます。
  • Lock() / Unlock() (Mutex): 複数のgoroutineが共有リソースに同時にアクセスする際に発生する競合状態(race condition)を防ぐための同期プリミティブです。sync.Mutex のメソッドで、Lock() はロックを取得し、Unlock() はロックを解放します。これにより、クリティカルセクション(共有リソースにアクセスするコード部分)へのアクセスを一度に一つのgoroutineに制限します。
  • エッジトリガー型ポーリング: I/Oイベント通知の一種で、ファイルディスクリプタの状態が変化したときに一度だけイベントを通知します。例えば、ソケットに新しいデータが到着したときに一度だけ通知し、その後のデータ到着は通知しません。これに対し、レベルトリガー型は、データが利用可能な限り(またはバッファが書き込み可能な限り)繰り返しイベントを通知します。エッジトリガー型は、適切に実装すればより効率的なI/O処理を可能にします。

技術的詳細

このコミットの主要な変更点は以下の通りです。

  1. デッドラインチェックの pollServer.PrepareRead/Write への分離:

    • 以前は、netFDRead, Write, ReadFrom, ReadMsg, WriteTo, WriteMsg メソッド内で直接 fd.rdeadline.expired()fd.wdeadline.expired() をチェックしていました。
    • このコミットでは、pollServerPrepareRead(fd *netFD) errorPrepareWrite(fd *netFD) error という新しいメソッドが追加されました。これらのメソッドは、I/O操作を開始する前にデッドラインが期限切れになっていないかをチェックし、期限切れであれば errTimeout を返します。
    • これにより、デッドラインチェックのロジックが一元化され、pollServer がI/O操作の準備段階でデッドラインを考慮できるようになります。これは、将来的にエッジトリガー型ポーリングを導入する際に、I/Oイベントの処理フローにデッドラインチェックをより自然に組み込むための準備となります。
  2. accept および connect におけるデッドラインの尊重:

    • netFD.connect メソッドでは、syscall.Connect を呼び出す前に fd.pollServer.PrepareWrite(fd) を呼び出すようになりました。これにより、connect 操作がブロックしない場合でも、書き込みデッドラインが過去に設定されていれば即座にタイムアウトエラーが返されるようになります。
    • netFD.accept メソッドでは、accept システムコールを呼び出す前に fd.pollServer.PrepareRead(fd) を呼び出すようになりました。これにより、accept 操作がブロックしない場合でも、読み込みデッドラインが過去に設定されていれば即座にタイムアウトエラーが返されるようになります。
    • この変更により、acceptconnect のデッドライン処理が readwrite と同様に、操作のブロックの有無にかかわらず一貫した振る舞いをするようになります。
  3. connect および accept 周りのロック (rio/wio):

    • netFD 構造体には、rio (read I/O) と wio (write I/O) という sync.Mutex 型のフィールドが追加されました(既存のフィールドの利用または追加)。
    • netFD.connect メソッドでは、fd.wio.Lock()defer fd.wio.Unlock() が追加され、connect 操作全体が書き込みI/Oロックで保護されるようになりました。
    • netFD.accept メソッドでは、fd.rio.Lock()defer fd.rio.Unlock() が追加され、accept 操作全体が読み込みI/Oロックで保護されるようになりました。
    • これらのロックは、connectaccept のような操作中に pollServer.WaitReadpollServer.WaitWrite が並行して呼び出されることによる競合状態を防ぐために導入されました。これにより、pollServer の内部状態が安全に管理され、複数のgoroutineからの同時アクセスによるデータ破損や予期せぬ動作が回避されます。
  4. timeout_test.go の更新:

    • TestAcceptDeadlineConnectionAvailableTestConnectDeadlineInThePast という新しいテストケースが追加されました。
    • TestAcceptDeadlineConnectionAvailable は、接続がすぐに利用可能な状況で accept に過去のデッドラインを設定した場合に、正しくタイムアウトエラーが発生することを確認します。
    • TestConnectDeadlineInThePast は、接続がブロックせずに確立できる状況で DialTimeout に過去のデッドラインを設定した場合に、正しくタイムアウトエラーが発生することを確認します。
    • これらのテストは、コミットによって修正された accept/connect のデッドライン処理の新しい振る舞いを検証し、回帰を防ぐためのものです。

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

src/pkg/net/fd_unix.go

  • pollServer 構造体に PrepareReadPrepareWrite メソッドが追加されました。
  • netFD.connect メソッドに fd.wio.Lock() / Unlock()fd.pollServer.PrepareWrite(fd) の呼び出しが追加されました。
  • netFD.Read, ReadFrom, ReadMsg, Write, WriteTo, WriteMsg メソッドから、ループ内のデッドラインチェック (fd.rdeadline.expired() / fd.wdeadline.expired()) が削除され、代わりにループの前に fd.pollServer.PrepareRead(fd) / fd.pollServer.PrepareWrite(fd) の呼び出しが追加されました。
  • netFD.accept メソッドに fd.rio.Lock() / Unlock()fd.pollServer.PrepareRead(fd) の呼び出しが追加されました。

src/pkg/net/timeout_test.go

  • TestAcceptDeadlineConnectionAvailable テスト関数が追加されました。
  • TestConnectDeadlineInThePast テスト関数が追加されました。
  • 既存のテストケースのコメントが一部修正されました。

コアとなるコードの解説

src/pkg/net/fd_unix.go

// 新しく追加されたメソッド
func (s *pollServer) PrepareRead(fd *netFD) error {
	if fd.rdeadline.expired() { // 読み込みデッドラインが期限切れかチェック
		return errTimeout // 期限切れならタイムアウトエラーを返す
	}
	return nil
}

func (s *pollServer) PrepareWrite(fd *netFD) error {
	if fd.wdeadline.expired() { // 書き込みデッドラインが期限切れかチェック
		return errTimeout // 期限切れならタイムアウトエラーを返す
	}
	return nil
}

これらのメソッドは、I/O操作(読み込みまたは書き込み)を開始する直前に呼び出され、関連するデッドラインが既に期限切れになっていないかをチェックします。これにより、操作がブロックしない場合でも、デッドラインが尊重されるようになります。

func (fd *netFD) connect(ra syscall.Sockaddr) error {
	fd.wio.Lock() // 書き込みI/Oロックを取得
	defer fd.wio.Unlock() // 関数終了時にロックを解放
	if err := fd.pollServer.PrepareWrite(fd); err != nil { // 接続前に書き込みデッドラインをチェック
		return err
	}
	err := syscall.Connect(fd.sysfd, ra)
	// ... 既存の接続ロジック ...
}

connect 操作の開始時に fd.wio.Lock() でロックを取得し、PrepareWrite を呼び出すことで、接続が即座に確立できる場合でもデッドラインが適用されるようになりました。ロックは、connect 処理中に WaitWrite が安全に呼び出されることを保証します。

func (fd *netFD) Read(p []byte) (n int, err error) {
	// ... 既存の参照カウント処理 ...
	defer fd.decref()
	if err := fd.pollServer.PrepareRead(fd); err != nil { // 読み込み前にデッドラインをチェック
		return 0, &OpError{"read", fd.net, fd.raddr, err}
	}
	for {
		// 以前ここに存在した fd.rdeadline.expired() のチェックは削除された
		n, err = syscall.Read(int(fd.sysfd), p)
		// ... 既存の読み込みロジック ...
	}
}

Read メソッド(および他の ReadFrom, ReadMsg, Write, WriteTo, WriteMsg メソッド)では、ループ内のデッドラインチェックが削除され、代わりにループの前に PrepareRead (または PrepareWrite) が呼び出されるようになりました。これにより、デッドラインチェックのロジックが pollServer に集約され、よりクリーンなコード構造になります。

func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (netfd *netFD, err error) {
	fd.rio.Lock() // 読み込みI/Oロックを取得
	defer fd.rio.Unlock() // 関数終了時にロックを解放
	if err := fd.incref(false); err != nil {
		return nil, err
	}
	// ... 既存の参照カウント処理 ...
	if err = fd.pollServer.PrepareRead(fd); err != nil { // accept前に読み込みデッドラインをチェック
		return nil, &OpError{"accept", fd.net, fd.laddr, err}
	}
	for {
		s, rsa, err = accept(fd.sysfd)
		// ... 既存のacceptロジック ...
	}
}

accept 操作の開始時に fd.rio.Lock() でロックを取得し、PrepareRead を呼び出すことで、接続が即座に利用可能な場合でもデッドラインが適用されるようになりました。ロックは、accept 処理中に WaitRead が安全に呼び出されることを保証します。

src/pkg/net/timeout_test.go

// 新しく追加されたテストケース
func TestAcceptDeadlineConnectionAvailable(t *testing.T) {
	// ... OSスキップロジック ...
	ln := newLocalListener(t).(*TCPListener)
	defer ln.Close()

	go func() {
		c, err := Dial("tcp", ln.Addr().String()) // クライアントが接続を試みる
		if err != nil {
			t.Fatalf("Dial: %v", err)
		}
		defer c.Close()
		var buf [1]byte
		c.Read(buf[:]) // 接続またはリスナーがクローズされるまでブロック
	}()
	time.Sleep(10 * time.Millisecond) // クライアント接続が利用可能になるのを待つ
	ln.SetDeadline(time.Now().Add(-5 * time.Second)) // 過去のデッドラインを設定
	c, err := ln.Accept() // acceptを呼び出す
	if err == nil {
		defer c.Close()
	}
	if !isTimeout(err) { // タイムアウトエラーが返されることを期待
		t.Fatalf("Accept: got %v; want timeout", err)
	}
}

このテストは、リスナーに過去のデッドラインを設定し、その間にクライアントが接続を試みるシナリオをシミュレートします。コミットの変更により、accept は接続がすぐに利用可能であっても、設定されたデッドラインが過去であるためタイムアウトエラーを返すはずです。

// 新しく追加されたテストケース
func TestConnectDeadlineInThePast(t *testing.T) {
	// ... OSスキップロジック ...
	ln := newLocalListener(t).(*TCPListener)
	defer ln.Close()

	go func() {
		c, err := ln.Accept() // サーバー側で接続を受け入れる準備
		if err == nil {
			defer c.Close()
		}
	}()
	time.Sleep(10 * time.Millisecond) // サーバーがacceptするのを待つ
	c, err := DialTimeout("tcp", ln.Addr().String(), -5*time.Second) // 過去のデッドラインで接続を試みる
	if err == nil {
		defer c.Close()
	}
	if !isTimeout(err) { // タイムアウトエラーが返されることを期待
		t.Fatalf("DialTimeout: got %v; want timeout", err)
	}
}

このテストは、DialTimeout を使用して過去のデッドラインを設定し、接続が即座に確立できる状況をシミュレートします。コミットの変更により、DialTimeout は接続がすぐに確立できる場合でも、設定されたデッドラインが過去であるためタイムアウトエラーを返すはずです。

関連リンク

参考にした情報源リンク

  • Go言語の net パッケージに関する公式ドキュメント
  • Go言語の sync パッケージに関する公式ドキュメント
  • Go言語のネットワークプログラミングに関する一般的な情報源
  • epollkqueue などのI/O多重化メカニズムに関する情報
  • Goの net パッケージのソースコード (src/pkg/net/)
  • Goの poll パッケージのソースコード (src/pkg/runtime/poll/) (内部実装の詳細を理解するため)
  • Goの syscall パッケージのドキュメント (システムコールエラーコードの理解のため)I have provided the detailed explanation as requested, following all the specified sections and instructions. I have used the commit information and my knowledge of Go's net package and operating system I/O to provide a comprehensive technical analysis. I have also included the relevant links and explained the core code changes.
# [インデックス 15627] ファイルの概要

このコミットは、Go言語の `net` パッケージにおける `accept` および `connect` 操作のデッドライン(期限)処理を修正し、これらの操作がブロックせずに実行できる場合でもデッドラインが尊重されるように変更します。これにより、`read` および `write` 操作との一貫性が確保されます。また、デッドラインチェックを `pollServer.PrepareRead/Write` に分離し、エッジトリガー型 `pollServer` の準備を進めるとともに、`connect`/`accept` 周りに `rio`/`wio` ロックを追加することで、`pollServer.WaitRead/Write` が並行して呼び出されないようにしています。

## コミット

commit 0f136f2c057459999f93da2d588325e192160b39 Author: Dmitriy Vyukov dvyukov@google.com Date: Thu Mar 7 17:03:40 2013 +0400

net: fix accept/connect deadline handling
Ensure that accept/connect respect deadline,
even if the operation can be executed w/o blocking.
Note this changes external behavior, but it makes
it consistent with read/write.
Factor out deadline check into pollServer.PrepareRead/Write,
in preparation for edge triggered pollServer.
Ensure that pollServer.WaitRead/Write are not called concurrently
by adding rio/wio locks around connect/accept.

R=golang-dev, mikioh.mikioh, bradfitz, iant
CC=golang-dev
https://golang.org/cl/7436048

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

[https://github.com/golang/go/commit/0f136f2c057459999f93da2d588325e192160b39](https://github.com/golang/go/commit/0f136f2c057459999f93da2d588325e192160b39)

## 元コミット内容

`net: fix accept/connect deadline handling`
`accept` および `connect` がデッドラインを尊重するように修正します。
操作がブロックせずに実行できる場合でも、デッドラインが尊重されるようにします。
これは外部の振る舞いを変更しますが、`read`/`write` との一貫性を保ちます。
デッドラインチェックを `pollServer.PrepareRead/Write` に分離し、エッジトリガー型 `pollServer` の準備を進めます。
`connect`/`accept` 周りに `rio`/`wio` ロックを追加することで、`pollServer.WaitRead/Write` が並行して呼び出されないようにします。

## 変更の背景

Goの `net` パッケージでは、ネットワークI/O操作に対してデッドライン(タイムアウト)を設定する機能が提供されています。しかし、このコミット以前は、`read` や `write` 操作とは異なり、`accept`(接続の受け入れ)や `connect`(接続の確立)操作において、**操作が即座に完了し、ブロックが発生しない場合**にデッドラインが適切に適用されないという不整合がありました。

具体的には、デッドラインが過去に設定されていたとしても、接続がすぐに利用可能であったり、接続が即座に確立できる状況では、デッドラインを無視して操作が成功してしまう可能性がありました。これは、ユーザーが設定したデッドラインの意図に反する動作であり、アプリケーションが予期せぬ挙動を示す原因となり得ました。

このコミットの目的は、この不整合を解消し、`accept` および `connect` 操作も `read`/`write` と同様に、操作のブロックの有無にかかわらずデッドラインを厳密に尊重するようにすることです。これにより、GoのネットワークI/Oのデッドライン処理全体の一貫性と予測可能性が向上します。

また、`pollServer` の内部構造を改善し、将来的なエッジトリガー型ポーリングメカニズムへの移行を容易にするための準備も含まれています。これは、I/Oイベントの処理効率を向上させるための基盤作りです。さらに、`connect` や `accept` のような操作中に `pollServer.WaitRead/Write` が並行して呼び出されることによる潜在的な競合状態を防ぐために、適切なロック機構を導入する必要がありました。

## 前提知識の解説

*   **Goの `net` パッケージ**: Go言語の標準ライブラリの一部で、TCP/IP、UDP、UnixドメインソケットなどのネットワークI/O機能を提供します。ソケットの作成、接続、データの送受信、リスニングなどの基本的なネットワーク操作を抽象化しています。
*   **デッドライン (Deadline)**: ネットワークI/O操作が完了するまでの最大時間を設定するメカニズムです。指定された時間内に操作が完了しない場合、操作はタイムアウトエラーを返します。Goの `net.Conn` インターフェースには `SetReadDeadline`, `SetWriteDeadline`, `SetDeadline` メソッドがあります。
*   **`pollServer`**: Goの `net` パッケージの内部コンポーネントで、OSのI/O多重化メカニズム(Linuxのepoll、macOSのkqueueなど)を抽象化し、ノンブロッキングI/Oを効率的に処理するためのものです。Goのgoroutineスケジューラと連携し、I/O操作がブロックする際にgoroutineを一時停止させ、I/Oが準備できたときに再開させます。
*   **`netFD`**: `net` パッケージ内でファイルディスクリプタ(ソケット)をラップする構造体です。I/O操作の状態、デッドライン、`pollServer` への参照などを保持します。
*   **`syscall.EINPROGRESS` / `syscall.EAGAIN`**:
    *   `EINPROGRESS`: ノンブロッキングソケットで `connect` を呼び出した際に、接続が即座に確立されず、バックグラウンドで進行中であることを示すエラーコードです。
    *   `EAGAIN` (または `EWOULDBLOCK`): ノンブロッキングソケットで `read` や `write` などのI/O操作を呼び出した際に、データがすぐに利用できない、またはバッファが満杯で書き込めないことを示すエラーコードです。これらのエラーは、操作がブロックする可能性があることを示唆しており、通常はポーリングメカニズム(`pollServer`)と組み合わせて使用されます。
*   **`Lock()` / `Unlock()` (Mutex)**: 複数のgoroutineが共有リソースに同時にアクセスする際に発生する競合状態(race condition)を防ぐための同期プリミティブです。`sync.Mutex` のメソッドで、`Lock()` はロックを取得し、`Unlock()` はロックを解放します。これにより、クリティカルセクション(共有リソースにアクセスするコード部分)へのアクセスを一度に一つのgoroutineに制限します。
*   **エッジトリガー型ポーリング**: I/Oイベント通知の一種で、ファイルディスクリプタの状態が変化したときに一度だけイベントを通知します。例えば、ソケットに新しいデータが到着したときに一度だけ通知し、その後のデータ到着は通知しません。これに対し、レベルトリガー型は、データが利用可能な限り(またはバッファが書き込み可能な限り)繰り返しイベントを通知します。エッジトリガー型は、適切に実装すればより効率的なI/O処理を可能にします。

## 技術的詳細

このコミットの主要な変更点は以下の通りです。

1.  **デッドラインチェックの `pollServer.PrepareRead/Write` への分離**:
    *   以前は、`netFD` の `Read`, `Write`, `ReadFrom`, `ReadMsg`, `WriteTo`, `WriteMsg` メソッド内で直接 `fd.rdeadline.expired()` や `fd.wdeadline.expired()` をチェックしていました。
    *   このコミットでは、`pollServer` に `PrepareRead(fd *netFD) error` と `PrepareWrite(fd *netFD) error` という新しいメソッドが追加されました。これらのメソッドは、I/O操作を開始する前にデッドラインが期限切れになっていないかをチェックし、期限切れであれば `errTimeout` を返します。
    *   これにより、デッドラインチェックのロジックが一元化され、`pollServer` がI/O操作の準備段階でデッドラインを考慮できるようになります。これは、将来的にエッジトリガー型ポーリングを導入する際に、I/Oイベントの処理フローにデッドラインチェックをより自然に組み込むための準備となります。

2.  **`accept` および `connect` におけるデッドラインの尊重**:
    *   `netFD.connect` メソッドでは、`syscall.Connect` を呼び出す前に `fd.pollServer.PrepareWrite(fd)` を呼び出すようになりました。これにより、`connect` 操作がブロックしない場合でも、書き込みデッドラインが過去に設定されていれば即座にタイムアウトエラーが返されるようになります。
    *   `netFD.accept` メソッドでは、`accept` システムコールを呼び出す前に `fd.pollServer.PrepareRead(fd)` を呼び出すようになりました。これにより、`accept` 操作がブロックしない場合でも、読み込みデッドラインが過去に設定されていれば即座にタイムアウトエラーが返されるようになります。
    *   この変更により、`accept` と `connect` のデッドライン処理が `read` と `write` と同様に、操作のブロックの有無にかかわらず一貫した振る舞いをするようになります。

3.  **`connect` および `accept` 周りのロック (`rio`/`wio`)**:
    *   `netFD` 構造体には、`rio` (read I/O) と `wio` (write I/O) という `sync.Mutex` 型のフィールドが追加されました(既存のフィールドの利用または追加)。
    *   `netFD.connect` メソッドでは、`fd.wio.Lock()` と `defer fd.wio.Unlock()` が追加され、`connect` 操作全体が書き込みI/Oロックで保護されるようになりました。
    *   `netFD.accept` メソッドでは、`fd.rio.Lock()` と `defer fd.rio.Unlock()` が追加され、`accept` 操作全体が読み込みI/Oロックで保護されるようになりました。
    *   これらのロックは、`connect` や `accept` のような操作中に `pollServer.WaitRead` や `pollServer.WaitWrite` が並行して呼び出されることによる競合状態を防ぐために導入されました。これにより、`pollServer` の内部状態が安全に管理され、複数のgoroutineからの同時アクセスによるデータ破損や予期せぬ動作が回避されます。

4.  **`timeout_test.go` の更新**:
    *   `TestAcceptDeadlineConnectionAvailable` と `TestConnectDeadlineInThePast` という新しいテストケースが追加されました。
    *   `TestAcceptDeadlineConnectionAvailable` は、接続がすぐに利用可能な状況で `accept` に過去のデッドラインを設定した場合に、正しくタイムアウトエラーが発生することを確認します。
    *   `TestConnectDeadlineInThePast` は、接続がブロックせずに確立できる状況で `DialTimeout` に過去のデッドラインを設定した場合に、正しくタイムアウトエラーが発生することを確認します。
    *   これらのテストは、コミットによって修正された `accept`/`connect` のデッドライン処理の新しい振る舞いを検証し、回帰を防ぐためのものです。

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

`src/pkg/net/fd_unix.go`
*   `pollServer` 構造体に `PrepareRead` と `PrepareWrite` メソッドが追加されました。
*   `netFD.connect` メソッドに `fd.wio.Lock()` / `Unlock()` と `fd.pollServer.PrepareWrite(fd)` の呼び出しが追加されました。
*   `netFD.Read`, `ReadFrom`, `ReadMsg`, `Write`, `WriteTo`, `WriteMsg` メソッドから、ループ内のデッドラインチェック (`fd.rdeadline.expired()` / `fd.wdeadline.expired()`) が削除され、代わりにループの前に `fd.pollServer.PrepareRead(fd)` / `fd.pollServer.PrepareWrite(fd)` の呼び出しが追加されました。
*   `netFD.accept` メソッドに `fd.rio.Lock()` / `Unlock()` と `fd.pollServer.PrepareRead(fd)` の呼び出しが追加されました。

`src/pkg/net/timeout_test.go`
*   `TestAcceptDeadlineConnectionAvailable` テスト関数が追加されました。
*   `TestConnectDeadlineInThePast` テスト関数が追加されました。
*   既存のテストケースのコメントが一部修正されました。

## コアとなるコードの解説

### `src/pkg/net/fd_unix.go`

```go
// 新しく追加されたメソッド
func (s *pollServer) PrepareRead(fd *netFD) error {
	if fd.rdeadline.expired() { // 読み込みデッドラインが期限切れかチェック
		return errTimeout // 期限切れならタイムアウトエラーを返す
	}
	return nil
}

func (s *pollServer) PrepareWrite(fd *netFD) error {
	if fd.wdeadline.expired() { // 書き込みデッドラインが期限切れかチェック
		return errTimeout // 期限切れならタイムアウトエラーを返す
	}
	return nil
}

これらのメソッドは、I/O操作(読み込みまたは書き込み)を開始する直前に呼び出され、関連するデッドラインが既に期限切れになっていないかをチェックします。これにより、操作がブロックしない場合でも、デッドラインが尊重されるようになります。

func (fd *netFD) connect(ra syscall.Sockaddr) error {
	fd.wio.Lock() // 書き込みI/Oロックを取得
	defer fd.wio.Unlock() // 関数終了時にロックを解放
	if err := fd.pollServer.PrepareWrite(fd); err != nil { // 接続前に書き込みデッドラインをチェック
		return err
	}
	err := syscall.Connect(fd.sysfd, ra)
	// ... 既存の接続ロジック ...
}

connect 操作の開始時に fd.wio.Lock() でロックを取得し、PrepareWrite を呼び出すことで、接続が即座に確立できる場合でもデッドラインが適用されるようになりました。ロックは、connect 処理中に WaitWrite が安全に呼び出されることを保証します。

func (fd *netFD) Read(p []byte) (n int, err error) {
	// ... 既存の参照カウント処理 ...
	defer fd.decref()
	if err := fd.pollServer.PrepareRead(fd); err != nil { // 読み込み前にデッドラインをチェック
		return 0, &OpError{"read", fd.net, fd.raddr, err}
	}
	for {
		// 以前ここに存在した fd.rdeadline.expired() のチェックは削除された
		n, err = syscall.Read(int(fd.sysfd), p)
		// ... 既存の読み込みロジック ...
	}
}

Read メソッド(および他の ReadFrom, ReadMsg, Write, WriteTo, WriteMsg メソッド)では、ループ内のデッドラインチェックが削除され、代わりにループの前に PrepareRead (または PrepareWrite) が呼び出されるようになりました。これにより、デッドラインチェックのロジックが pollServer に集約され、よりクリーンなコード構造になります。

func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (netfd *netFD, err error) {
	fd.rio.Lock() // 読み込みI/Oロックを取得
	defer fd.rio.Unlock() // 関数終了時にロックを解放
	if err := fd.incref(false); err != nil {
		return nil, err
	}
	// ... 既存の参照カウント処理 ...
	if err = fd.pollServer.PrepareRead(fd); err != nil { // accept前に読み込みデッドラインをチェック
		return nil, &OpError{"accept", fd.net, fd.laddr, err}
	}
	for {
		s, rsa, err = accept(fd.sysfd)
		// ... 既存のacceptロジック ...
	}
}

accept 操作の開始時に fd.rio.Lock() でロックを取得し、PrepareRead を呼び出すことで、接続が即座に利用可能な場合でもデッドラインが適用されるようになりました。ロックは、accept 処理中に WaitRead が安全に呼び出されることを保証します。

src/pkg/net/timeout_test.go

// 新しく追加されたテストケース
func TestAcceptDeadlineConnectionAvailable(t *testing.T) {
	// ... OSスキップロジック ...
	ln := newLocalListener(t).(*TCPListener)
	defer ln.Close()

	go func() {
		c, err := Dial("tcp", ln.Addr().String()) // クライアントが接続を試みる
		if err != nil {
			t.Fatalf("Dial: %v", err)
		}
		defer c.Close()
		var buf [1]byte
		c.Read(buf[:]) // 接続またはリスナーがクローズされるまでブロック
	}()
	time.Sleep(10 * time.Millisecond) // クライアント接続が利用可能になるのを待つ
	ln.SetDeadline(time.Now().Add(-5 * time.Second)) // 過去のデッドラインを設定
	c, err := ln.Accept() // acceptを呼び出す
	if err == nil {
		defer c.Close()
	}
	if !isTimeout(err) { // タイムアウトエラーが返されることを期待
		t.Fatalf("Accept: got %v; want timeout", err)
	}
}

このテストは、リスナーに過去のデッドラインを設定し、その間にクライアントが接続を試みるシナリオをシミュレートします。コミットの変更により、accept は接続がすぐに利用可能であっても、設定されたデッドラインが過去であるためタイムアウトエラーを返すはずです。

// 新しく追加されたテストケース
func TestConnectDeadlineInThePast(t *testing.T) {
	// ... OSスキップロジック ...
	ln := newLocalListener(t).(*TCPListener)
	defer ln.Close()

	go func() {
		c, err := ln.Accept() // サーバー側で接続を受け入れる準備
		if err == nil {
			defer c.Close()
		}
	}()
	time.Sleep(10 * time.Millisecond) // サーバーがacceptするのを待つ
	c, err := DialTimeout("tcp", ln.Addr().String(), -5*time.Second) // 過去のデッドラインで接続を試みる
	if err == nil {
		defer c.Close()
	}
	if !isTimeout(err) { // タイムアウトエラーが返されることを期待
		t.Fatalf("DialTimeout: got %v; want timeout", err)
	}
}

このテストは、DialTimeout を使用して過去のデッドラインを設定し、接続が即座に確立できる状況をシミュレートします。コミットの変更により、DialTimeout は接続がすぐに確立できる場合でも、設定されたデッドラインが過去であるためタイムアウトエラーを返すはずです。

関連リンク

参考にした情報源リンク

  • Go言語の net パッケージに関する公式ドキュメント
  • Go言語の sync パッケージに関する公式ドキュメント
  • Go言語のネットワークプログラミングに関する一般的な情報源
  • epollkqueue などのI/O多重化メカニズムに関する情報
  • Goの net パッケージのソースコード (src/pkg/net/)
  • Goの poll パッケージのソースコード (src/pkg/runtime/poll/) (内部実装の詳細を理解するため)
  • Goの syscall パッケージのドキュメント (システムコールエラーコードの理解のため)