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

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

このコミットは、Go言語のネットワークパッケージ(net)における重要な改善を含んでいます。主に、ネットワークI/O操作におけるタイムアウト機能の導入と、並行書き込み時のデータ破損を防ぐためのロック機構の追加、そしてConnインターフェースのドキュメント強化に焦点を当てています。これにより、ネットワーク通信の信頼性と堅牢性が向上し、開発者がより予測可能なネットワークアプリケーションを構築できるようになります。

コミット

commit 1e37e8a417dc36bc6da6828cd7c20dd53d4ba6a9
Author: Russ Cox <rsc@golang.org>
Date:   Fri Mar 6 17:51:31 2009 -0800

    document Conn interface better, in preparation
    for per-method interface documentation
    by mkdoc.pl.
    
    implement timeouts on network reads
    and use them in dns client.
    
    also added locks on i/o to ensure writes
    are not interlaced.
    
    R=r
    DELTA=340  (272 added, 25 deleted, 43 changed)
    OCL=25799
    CL=25874

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

https://github.com/golang/go/commit/1e37e8a417dc36bc6da6828cd7c20dd53d4ba6a9

元コミット内容

このコミットの元の内容は以下の通りです。

  • Connインターフェースのドキュメントを改善し、mkdoc.plによるメソッドごとのインターフェースドキュメント生成に備える。
  • ネットワーク読み込みにタイムアウトを実装し、DNSクライアントでそれを使用する。
  • 書き込みが混在しないように、I/Oにロックを追加する。

変更の背景

このコミットが行われた2009年3月は、Go言語がまだ公開されて間もない、非常に初期の段階でした。当時のGoのネットワークスタックは基本的な機能を提供していましたが、実用的なアプリケーション開発にはいくつかの課題がありました。

  1. タイムアウト機能の欠如: ネットワークI/O操作(特に読み込み)にタイムアウトが設定されていない場合、ネットワークの遅延や相手からの応答がない場合に、アプリケーションが無限にブロックされる可能性がありました。これは、応答性の高いサービスや堅牢なクライアントを構築する上で大きな問題となります。DNSクライアントのような、外部サービスとの通信を伴うコンポーネントでは、タイムアウトは必須の機能です。
  2. 並行書き込み時のデータ破損: 複数のゴルーチンが同時に同じネットワーク接続に書き込みを行う場合、書き込み操作がインターリーブ(混在)し、データが破損する可能性がありました。これは、TCPのようなストリーム指向のプロトコルでは特に問題となり、アプリケーションレベルでの同期が必要とされます。
  3. ドキュメントの不足: ConnインターフェースはGoのネットワークプログラミングの根幹をなすものであり、その各メソッドの振る舞いや期待される動作について、より詳細なドキュメントが必要とされていました。これは、Goの標準ライブラリの品質向上と、開発者による適切な利用を促進するために不可欠でした。

これらの課題に対処するため、このコミットではネットワークI/Oの信頼性、堅牢性、および使いやすさを向上させるための重要な変更が導入されました。

前提知識の解説

このコミットを理解するためには、以下の技術的な概念について基本的な知識が必要です。

1. ネットワークI/Oとブロッキング/ノンブロッキングI/O

  • ブロッキングI/O: I/O操作が完了するまで、呼び出し元のスレッド(またはゴルーチン)がブロックされる(待機する)方式です。シンプルですが、応答性が低下する可能性があります。
  • ノンブロッキングI/O: I/O操作がすぐに戻り、データが利用可能でない場合や書き込みバッファが満杯の場合にはエラー(例: EAGAINEWOULDBLOCK)を返します。これにより、アプリケーションはI/O操作の完了を待つ間に他の処理を行うことができます。Goのネットワークパッケージは内部的にノンブロッキングI/Oとイベント通知メカニズム(kqueueepoll)を組み合わせて、効率的な並行I/Oを実現しています。

2. タイムアウト

ネットワーク通信において、特定の操作(読み込み、書き込み、接続確立など)が指定された時間内に完了しない場合に、その操作を中断しエラーを返す機能です。これにより、アプリケーションが無限に待機するのを防ぎ、リソースの枯渇や応答性の低下を防ぐことができます。

3. kqueueepoll

これらは、Unix系OSにおける効率的なI/Oイベント通知メカニズムです。

  • kqueue (FreeBSD, macOSなど): 多数のファイルディスクリプタ(ソケットなど)からのイベント(読み込み可能、書き込み可能など)を効率的に監視するためのシステムコールです。
  • epoll (Linux): kqueueと同様に、多数のファイルディスクリプタからのイベントを効率的に監視するためのLinux固有のシステムコールです。 Goのネットワークパッケージは、これらのOS固有のメカニズムを抽象化し、クロスプラットフォームで動作するノンブロッキングI/Oを提供しています。

4. ミューテックス (sync.Mutex)

並行プログラミングにおいて、共有リソースへのアクセスを同期するためのメカニズムです。ミューテックスは、一度に一つのゴルーチンだけが特定のコードセクション(クリティカルセクション)を実行できるようにすることで、データ競合を防ぎます。このコミットでは、ネットワーク書き込み操作がインターリーブされるのを防ぐために使用されています。

5. os.EAGAIN

Unix系システムコールがノンブロッキングモードで実行され、要求された操作(読み込みや書き込み)がすぐに完了できない場合に返されるエラーコードです。これは、操作が失敗したことを意味するのではなく、後で再試行する必要があることを示します。Goのネットワークパッケージでは、このエラーを受け取った際に、pollServerを通じてイベントの発生を待機し、I/O操作を再試行します。

技術的詳細

このコミットの技術的な変更は、主に以下の3つの柱に基づいています。

1. ネットワークI/Oタイムアウトの実装

  • netFD構造体の拡張: src/lib/net/fd.goにおいて、netFD構造体にrdeadline_delta(読み込みタイムアウト期間)、rdeadline(読み込み期限時刻)、wdeadline_delta(書き込みタイムアウト期間)、wdeadline(書き込み期限時刻)が追加されました。これらはナノ秒単位で管理されます。
  • pollServerのタイムアウト処理: pollServerは、ノンブロッキングI/Oのイベントを監視し、I/O操作が準備できた際にゴルーチンを再開する役割を担っています。このコミットでは、pollServerdeadlineフィールドが追加され、監視対象のファイルディスクリプタの中で最も近い期限時刻が設定されます。
    • pollServer.Now(): 現在時刻をナノ秒単位で取得するヘルパー関数が追加されました。
    • pollServer.CheckDeadlines(): pollServerRunループ内で定期的に呼び出され、期限切れのI/O操作をチェックし、該当するnetFDrdeadlineまたはwdeadline-1に設定してタイムアウト状態を示します。
    • pollster.WaitFD(nsec int64): OS固有のI/O多重化メカニズム(kqueueepoll)の待機関数にタイムアウト引数(nsec)が追加されました。これにより、指定された時間内にイベントが発生しない場合、待機が中断されます。
  • netFD.ReadnetFD.Writeの変更:
    • これらのメソッドは、rdeadline_deltawdeadline_deltaが設定されている場合、現在の時刻にそのデルタを加算してrdeadlinewdeadlineを設定します。
    • os.EAGAINエラーが返された場合、pollserver.WaitRead(fd)またはpollserver.WaitWrite(fd)を呼び出してイベントの発生を待機しますが、この待機は設定された期限時刻までとなります。期限が切れた場合、fd.rdeadline >= 0またはfd.wdeadline >= 0の条件が満たされなくなり、ループを抜けてos.EAGAINエラーを返します。
  • ConnインターフェースのSetReadTimeoutSetWriteTimeout: これらのメソッドは、以前はOSのソケットオプション(SO_RCVTIMEO, SO_SNDTIMEO)を直接設定していましたが、このコミットからはnetFDrdeadline_deltawdeadline_deltaを設定するように変更されました。これにより、Goランタイムがタイムアウトをより細かく制御できるようになります。

2. 並行書き込み時のロック機構

  • netFD構造体へのミューテックス追加: src/lib/net/fd.gonetFD構造体にrio sync.Mutexwio sync.Mutexが追加されました。これらはそれぞれ読み込みと書き込み操作を保護するためのミューテックスです。
  • netFD.ReadnetFD.Writeでのロック: netFD.ReadnetFD.Writeメソッドの冒頭でそれぞれのミューテックスをロックし、メソッドの終了時にdeferを使ってアンロックするように変更されました。これにより、複数のゴルーチンが同時に同じnetFDに対して読み込みや書き込みを行おうとした場合でも、操作が直列化され、データがインターリーブされるのを防ぎます。

3. Connインターフェースのドキュメント強化

  • src/lib/net/net.goConnインターフェースの定義において、各メソッド(Read, Write, Close, ReadFrom, WriteTo, SetReadBuffer, SetWriteBuffer, SetTimeout, SetReadTimeout, SetWriteTimeout, SetLinger, SetReuseAddr, SetDontRoute, SetKeepAlive, BindToDevice)に対して、その機能、引数、戻り値、および特定の振る舞いに関する詳細なコメントが追加されました。これは、Goのドキュメント生成ツールmkdoc.plが各メソッドのドキュメントを自動生成できるようにするための準備でもありました。

4. syscallパッケージの変更

  • src/lib/syscall/socket_darwin.gosrc/lib/syscall/socket_linux.goにおいて、Setsockopt_linger関数のロジックが変更されました。以前はsec != 0の場合にLinger構造体のYesフィールドを1に設定していましたが、このコミットからはsec >= 0の場合に設定するように変更されました。これにより、sec == 0の場合(即座に接続を閉じて未送信データを破棄する)も適切にSO_LINGERオプションが設定されるようになります。

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

src/lib/net/fd.go

type netFD struct {
	// ... 既存のフィールド ...

	// owned by client
	rdeadline_delta int64;
	rdeadline int64;
	rio sync.Mutex; // 読み込み操作を保護するミューテックス
	wdeadline_delta int64;
	wdeadline int64;
	wio sync.Mutex; // 書き込み操作を保護するミューテックス

	// ... 既存のフィールド ...
}

// Make reads/writes blocking; last gasp, so no error checking.
func setBlock(fd int64) {
	flags, e := syscall.Fcntl(fd, syscall.F_GETFL, 0);
	if e != 0 {
		return;
	}
	syscall.Fcntl(fd, syscall.F_SETFL, flags & ^syscall.O_NONBLOCK);
}

type pollServer struct {
	// ... 既存のフィールド ...
	deadline int64;	// next deadline (nsec since 1970)
}

func (s *pollServer) Now() int64 {
	sec, nsec, err := os.Time();
	if err != nil {
		panic("net: os.Time: ", err.String());
	}
	nsec += sec * 1e9;
	return nsec;
}

func (s *pollServer) CheckDeadlines() {
	now := s.Now();
	// ... 期限切れのFDをチェックし、WakeFDを呼び出すロジック ...
}

func (s *pollServer) Run() {
	var scratch [100]byte;
	for {
		var t = s.deadline;
		if t > 0 {
			t = t - s.Now();
			if t < 0 {
				s.CheckDeadlines();
				continue;
			}
		}
		fd, mode, err := s.poll.WaitFD(t); // タイムアウト引数を渡す
		// ... イベント処理ロジック ...
	}
}

func (fd *netFD) Read(p []byte) (n int, err *os.Error) {
	if fd == nil || fd.osfd == nil {
		return -1, os.EINVAL
	}
	fd.rio.Lock(); // 読み込みロック
	defer fd.rio.Unlock(); // 読み込みアンロック
	if fd.rdeadline_delta > 0 {
		fd.rdeadline = pollserver.Now() + fd.rdeadline_delta;
	} else {
		fd.rdeadline = 0;
	}
	n, err = fd.osfd.Read(p);
	for err == os.EAGAIN && fd.rdeadline >= 0 { // タイムアウトチェックを追加
		pollserver.WaitRead(fd);
		n, err = fd.osfd.Read(p)
	}
	return n, err
}

func (fd *netFD) Write(p []byte) (n int, err *os.Error) {
	if fd == nil || fd.osfd == nil {
		return -1, os.EINVAL
	}
	fd.wio.Lock(); // 書き込みロック
	defer fd.wio.Unlock(); // 書き込みアンロック
	if fd.wdeadline_delta > 0 {
		fd.wdeadline = pollserver.Now() + fd.wdeadline_delta;
	} else {
		fd.wdeadline = 0;
	}
	err = nil;
	nn := 0;
	for nn < len(p) {
		n, err = fd.osfd.Write(p[nn:len(p)]);
		if n > 0 {
			nn += n
		}
		if nn == len(p) {
			break;
		}
		if err == os.EAGAIN && fd.wdeadline >= 0 { // タイムアウトチェックを追加
			pollserver.WaitWrite(fd);
			continue;
		}
		if n == 0 || err != nil {
			break;
		}
	}
	return nn, err
}

src/lib/net/net.go

type Conn interface {
	// Read blocks until data is ready from the connection
	// and then reads into b.  It returns the number
	// of bytes read, or 0 if the connection has been closed.
	Read(b []byte) (n int, err *os.Error);

	// Write writes the data in b to the connection.
	Write(b []byte) (n int, err *os.Error);

	// Close closes the connection.
	Close() *os.Error;

	// For packet-based protocols such as UDP,
	// ReadFrom reads the next packet from the network,
	// returning the number of bytes read and the remote
	// address that sent them.
	ReadFrom(b []byte) (n int, addr string, err *os.Error);

	// For packet-based protocols such as UDP,
	// WriteTo writes the byte buffer b to the network
	// as a single payload, sending it to the target address.
	WriteTo(addr string, b []byte) (n int, err *os.Error);

	// SetReadBuffer sets the size of the operating system's
	// receive buffer associated with the connection.
	SetReadBuffer(bytes int) *os.Error;

	// SetReadBuffer sets the size of the operating system's
	// transmit buffer associated with the connection.
	SetWriteBuffer(bytes int) *os.Error;

	// SetTimeout sets the read and write deadlines associated
	// with the connection.
	SetTimeout(nsec int64) *os.Error;

	// SetReadTimeout sets the time (in nanoseconds) that
	// Read will wait for data before returning os.EAGAIN.
	// Setting nsec == 0 (the default) disables the deadline.
	SetReadTimeout(nsec int64) *os.Error;

	// SetWriteTimeout sets the time (in nanoseconds) that
	// Write will wait to send its data before returning os.EAGAIN.
	// Setting nsec == 0 (the default) disables the deadline.
	// Even if write times out, it may return n > 0, indicating that
	// some of the data was successfully written.
	SetWriteTimeout(nsec int64) *os.Error;

	// SetLinger sets the behavior of Close() on a connection
	// which still has data waiting to be sent or to be acknowledged.
	//
	// If sec < 0 (the default), Close returns immediately and
	// the operating system finishes sending the data in the background.
	//
	// If sec == 0, Close returns immediately and the operating system
	// discards any unsent or unacknowledged data.
	//
	// If sec > 0, Close blocks for at most sec seconds waiting for
	// data to be sent and acknowledged.
	SetLinger(sec int) *os.Error;

	// SetReuseAddr sets whether it is okay to reuse addresses
	// from recent connections that were not properly closed.
	SetReuseAddr(reuseaddr bool) *os.Error;

	// SetDontRoute sets whether outgoing messages should
	// bypass the system routing tables.
	SetDontRoute(dontroute bool) *os.Error;

	// SetKeepAlive sets whether the operating system should send
	// keepalive messages on the connection.
	SetKeepAlive(keepalive bool) *os.Error;

	// BindToDevice binds a connection to a particular network device.
	BindToDevice(dev string) *os.Error;
}

コアとなるコードの解説

タイムアウトの実装

GoのネットワークI/Oは、OSのノンブロッキングI/Oとイベント通知メカニズム(kqueueepoll)を組み合わせて実装されています。以前は、SetReadTimeoutSetWriteTimeoutがOSのソケットオプション(SO_RCVTIMEO, SO_SNDTIMEO)を直接設定していましたが、これにはいくつかの制限がありました。例えば、OSによってはミリ秒単位の精度しかなかったり、読み込みと書き込みで異なるタイムアウトを設定するのが難しかったりする場合があります。

このコミットでは、Goランタイム自身がタイムアウトを管理するようになりました。

  • netFD構造体にrdeadline_deltawdeadline_deltaが追加され、これはユーザーが設定したタイムアウト期間(ナノ秒)を保持します。
  • netFD.ReadnetFD.Writeが呼び出されると、pollserver.Now()で現在の時刻を取得し、rdeadline_deltaまたはwdeadline_deltaを加算して、I/O操作の期限時刻(rdeadlineまたはwdeadline)を計算します。
  • I/O操作がos.EAGAIN(ノンブロッキングI/Oでデータがまだ準備できていないことを示す)を返した場合、pollserver.WaitRead(fd)またはpollserver.WaitWrite(fd)が呼び出されます。これらの関数は、内部的にpollServerにI/Oイベントの待機を登録します。
  • pollServerRunメソッドは、pollster.WaitFD(t)を呼び出してOSのイベント通知メカニズムを待機します。ここでtは、現在登録されているすべてのI/O操作の中で最も近い期限時刻までの残り時間です。これにより、OSレベルでタイムアウトを効率的に処理できます。
  • pollServer.CheckDeadlines()は、pollServerのループ内で定期的に実行され、期限切れのI/O操作を特定し、それらのnetFDrdeadlineまたはwdeadline-1に設定します。これにより、netFD.ReadnetFD.Writeのループが終了し、os.EAGAINエラーが返されることで、アプリケーションにタイムアウトが通知されます。

このアプローチにより、GoはOSのソケットオプションに依存することなく、より高精度で柔軟なタイムアウト制御を実現しています。

並行書き込み時のロック

netFD構造体に追加されたsync.Mutex型のriowioは、それぞれ読み込みと書き込み操作を保護するためのものです。

  • netFD.Readメソッドの冒頭でfd.rio.Lock()が呼び出され、読み込み操作が完了するまで他のゴルーチンが同じnetFDに対して読み込みを行うのをブロックします。
  • 同様に、netFD.Writeメソッドの冒頭でfd.wio.Lock()が呼び出され、書き込み操作が完了するまで他のゴルーチンが同じnetFDに対して書き込みを行うのをブロックします。

これにより、複数のゴルーチンが同時に同じネットワーク接続に対して書き込みを行っても、それらの書き込みがインターリーブされることなく、データが正しく送信されることが保証されます。これは、TCPのようなストリーム指向のプロトコルにおいて、アプリケーションレベルでのデータ整合性を保つ上で非常に重要です。

Connインターフェースのドキュメント強化

src/lib/net/net.goにおけるConnインターフェースの各メソッドへの詳細なコメント追加は、Goの標準ライブラリの品質と使いやすさを向上させるための重要なステップです。これらのコメントは、各メソッドの目的、引数、戻り値、および特定の振る舞いについて明確な説明を提供します。これにより、開発者はConnインターフェースをより正確に理解し、適切に使用できるようになります。また、これはGoのドキュメント生成ツールが自動的に高品質なAPIドキュメントを生成するための基盤となります。

関連リンク

  • Go言語の初期のネットワークパッケージに関する議論や設計ドキュメント(当時のGoコミュニティのメーリングリストやデザインドキュメントを検索すると、より深い背景情報が見つかる可能性があります)。
  • kqueueepollに関するOSのドキュメントや解説記事。
  • Go言語のsyncパッケージとミューテックスに関する公式ドキュメント。

参考にした情報源リンク

  • Go言語のソースコード(特にsrc/netおよびsrc/syscallパッケージの関連ファイル)。
  • Go言語の公式ドキュメント(当時のバージョンに遡って確認できる場合)。
  • Unix系OSのシステムコールに関するドキュメント(fcntl, kqueue, epollなど)。
  • 並行プログラミングにおけるミューテックスの概念に関する一般的な情報源。
  • Go言語の初期のコミット履歴と関連するコードレビュー。

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

このコミットは、Go言語のネットワークパッケージ(net)における重要な改善を含んでいます。主に、ネットワークI/O操作におけるタイムアウト機能の導入と、並行書き込み時のデータ破損を防ぐためのロック機構の追加、そしてConnインターフェースのドキュメント強化に焦点を当てています。これにより、ネットワーク通信の信頼性と堅牢性が向上し、開発者がより予測可能なネットワークアプリケーションを構築できるようになります。

コミット

commit 1e37e8a417dc36bc6da6828cd7c20dd53d4ba6a9
Author: Russ Cox <rsc@golang.org>
Date:   Fri Mar 6 17:51:31 2009 -0800

    document Conn interface better, in preparation
    for per-method interface documentation
    by mkdoc.pl.
    
    implement timeouts on network reads
    and use them in dns client.
    
    also added locks on i/o to ensure writes
    are not interlaced.
    
    R=r
    DELTA=340  (272 added, 25 deleted, 43 changed)
    OCL=25799
    CL=25874

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

https://github.com/golang/go/commit/1e37e8a417dc36bc6da6828cd7c20dd53d4ba6a9

元コミット内容

このコミットの元の内容は以下の通りです。

  • Connインターフェースのドキュメントを改善し、mkdoc.plによるメソッドごとのインターフェースドキュメント生成に備える。
  • ネットワーク読み込みにタイムアウトを実装し、DNSクライアントでそれを使用する。
  • 書き込みが混在しないように、I/Oにロックを追加する。

変更の背景

このコミットが行われた2009年3月は、Go言語がまだ公開されて間もない、非常に初期の段階でした。当時のGoのネットワークスタックは基本的な機能を提供していましたが、実用的なアプリケーション開発にはいくつかの課題がありました。

  1. タイムアウト機能の欠如: ネットワークI/O操作(特に読み込み)にタイムアウトが設定されていない場合、ネットワークの遅延や相手からの応答がない場合に、アプリケーションが無限にブロックされる可能性がありました。これは、応答性の高いサービスや堅牢なクライアントを構築する上で大きな問題となります。DNSクライアントのような、外部サービスとの通信を伴うコンポーネントでは、タイムアウトは必須の機能です。
  2. 並行書き込み時のデータ破損: 複数のゴルーチンが同時に同じネットワーク接続に書き込みを行う場合、書き込み操作がインターリーブ(混在)し、データが破損する可能性がありました。これは、TCPのようなストリーム指向のプロトコルでは特に問題となり、アプリケーションレベルでの同期が必要とされます。
  3. ドキュメントの不足: ConnインターフェースはGoのネットワークプログラミングの根幹をなすものであり、その各メソッドの振る舞いや期待される動作について、より詳細なドキュメントが必要とされていました。これは、Goの標準ライブラリの品質向上と、開発者による適切な利用を促進するために不可欠でした。

これらの課題に対処するため、このコミットではネットワークI/Oの信頼性、堅牢性、および使いやすさを向上させるための重要な変更が導入されました。

前提知識の解説

このコミットを理解するためには、以下の技術的な概念について基本的な知識が必要です。

1. ネットワークI/Oとブロッキング/ノンブロッキングI/O

  • ブロッキングI/O: I/O操作が完了するまで、呼び出し元のスレッド(またはゴルーチン)がブロックされる(待機する)方式です。シンプルですが、応答性が低下する可能性があります。
  • ノンブロッキングI/O: I/O操作がすぐに戻り、データが利用可能でない場合や書き込みバッファが満杯の場合にはエラー(例: EAGAINEWOULDBLOCK)を返します。これにより、アプリケーションはI/O操作の完了を待つ間に他の処理を行うことができます。Goのネットワークパッケージは内部的にノンブロッキングI/Oとイベント通知メカニズム(kqueueepoll)を組み合わせて、効率的な並行I/Oを実現しています。

2. タイムアウト

ネットワーク通信において、特定の操作(読み込み、書き込み、接続確立など)が指定された時間内に完了しない場合に、その操作を中断しエラーを返す機能です。これにより、アプリケーションが無限に待機するのを防ぎ、リソースの枯渇や応答性の低下を防ぐことができます。

3. kqueueepoll

これらは、Unix系OSにおける効率的なI/Oイベント通知メカニズムです。

  • kqueue (FreeBSD, macOSなど): 多数のファイルディスクリプタ(ソケットなど)からのイベント(読み込み可能、書き込み可能など)を効率的に監視するためのシステムコールです。
  • epoll (Linux): kqueueと同様に、多数のファイルディスクリプタからのイベントを効率的に監視するためのLinux固有のシステムコールです。 Goのネットワークパッケージは、これらのOS固有のメカニズムを抽象化し、クロスプラットフォームで動作するノンブロッキングI/Oを提供しています。

4. ミューテックス (sync.Mutex)

並行プログラミングにおいて、共有リソースへのアクセスを同期するためのメカニズムです。ミューテックスは、一度に一つのゴルーチンだけが特定のコードセクション(クリティカルセクション)を実行できるようにすることで、データ競合を防ぎます。このコミットでは、ネットワーク書き込み操作がインターリーブされるのを防ぐために使用されています。

5. os.EAGAIN

Unix系システムコールがノンブロッキングモードで実行され、要求された操作(読み込みや書き込み)がすぐに完了できない場合に返されるエラーコードです。これは、操作が失敗したことを意味するのではなく、後で再試行する必要があることを示します。Goのネットワークパッケージでは、このエラーを受け取った際に、pollServerを通じてイベントの発生を待機し、I/O操作を再試行します。

技術的詳細

このコミットの技術的な変更は、主に以下の3つの柱に基づいています。

1. ネットワークI/Oタイムアウトの実装

  • netFD構造体の拡張: src/lib/net/fd.goにおいて、netFD構造体にrdeadline_delta(読み込みタイムアウト期間)、rdeadline(読み込み期限時刻)、wdeadline_delta(書き込みタイムアウト期間)、wdeadline(書き込み期限時刻)が追加されました。これらはナノ秒単位で管理されます。
  • pollServerのタイムアウト処理: pollServerは、ノンブロッキングI/Oのイベントを監視し、I/O操作が準備できた際にゴルーチンを再開する役割を担っています。このコミットでは、pollServerdeadlineフィールドが追加され、監視対象のファイルディスクリプタの中で最も近い期限時刻が設定されます。
    • pollServer.Now(): 現在時刻をナノ秒単位で取得するヘルパー関数が追加されました。
    • pollServer.CheckDeadlines(): pollServerRunループ内で定期的に呼び出され、期限切れのI/O操作をチェックし、該当するnetFDrdeadlineまたはwdeadline-1に設定してタイムアウト状態を示します。
    • pollster.WaitFD(nsec int64): OS固有のI/O多重化メカニズム(kqueueepoll)の待機関数にタイムアウト引数(nsec)が追加されました。これにより、指定された時間内にイベントが発生しない場合、待機が中断されます。
  • netFD.ReadnetFD.Writeの変更:
    • これらのメソッドは、rdeadline_deltawdeadline_deltaが設定されている場合、現在の時刻にそのデルタを加算してrdeadlinewdeadlineを設定します。
    • os.EAGAINエラーが返された場合、pollserver.WaitRead(fd)またはpollserver.WaitWrite(fd)を呼び出してイベントの発生を待機しますが、この待機は設定された期限時刻までとなります。期限が切れた場合、fd.rdeadline >= 0またはfd.wdeadline >= 0の条件が満たされなくなり、ループを抜けてos.EAGAINエラーを返します。
  • ConnインターフェースのSetReadTimeoutSetWriteTimeout: これらのメソッドは、以前はOSのソケットオプション(SO_RCVTIMEO, SO_SNDTIMEO)を直接設定していましたが、このコミットからはnetFDrdeadline_deltawdeadline_deltaを設定するように変更されました。これにより、Goランタイムがタイムアウトをより細かく制御できるようになります。

2. 並行書き込み時のロック機構

  • netFD構造体へのミューテックス追加: src/lib/net/fd.gonetFD構造体にrio sync.Mutexwio sync.Mutexが追加されました。これらはそれぞれ読み込みと書き込み操作を保護するためのミューテックスです。
  • netFD.ReadnetFD.Writeでのロック: netFD.ReadnetFD.Writeメソッドの冒頭でそれぞれのミューテックスをロックし、メソッドの終了時にdeferを使ってアンロックするように変更されました。これにより、複数のゴルーチンが同時に同じnetFDに対して読み込みや書き込みを行おうとした場合でも、操作が直列化され、データがインターリーブされるのを防ぎます。

3. Connインターフェースのドキュメント強化

  • src/lib/net/net.goConnインターフェースの定義において、各メソッド(Read, Write, Close, ReadFrom, WriteTo, SetReadBuffer, SetWriteBuffer, SetTimeout, SetReadTimeout, SetWriteTimeout, SetLinger, SetReuseAddr, SetDontRoute, SetKeepAlive, BindToDevice)に対して、その機能、引数、戻り値、および特定の振る舞いに関する詳細なコメントが追加されました。これは、Goのドキュメント生成ツールmkdoc.plが各メソッドのドキュメントを自動生成できるようにするための準備でもありました。

4. syscallパッケージの変更

  • src/lib/syscall/socket_darwin.gosrc/lib/syscall/socket_linux.goにおいて、Setsockopt_linger関数のロジックが変更されました。以前はsec != 0の場合にLinger構造体のYesフィールドを1に設定していましたが、このコミットからはsec >= 0の場合に設定するように変更されました。これにより、sec == 0の場合(即座に接続を閉じて未送信データを破棄する)も適切にSO_LINGERオプションが設定されるようになります。

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

src/lib/net/fd.go

type netFD struct {
	// ... 既存のフィールド ...

	// owned by client
	rdeadline_delta int64;
	rdeadline int64;
	rio sync.Mutex; // 読み込み操作を保護するミューテックス
	wdeadline_delta int64;
	wdeadline int64;
	wio sync.Mutex; // 書き込み操作を保護するミューテックス

	// ... 既存のフィールド ...
}

// Make reads/writes blocking; last gasp, so no error checking.
func setBlock(fd int64) {
	flags, e := syscall.Fcntl(fd, syscall.F_GETFL, 0);
	if e != 0 {
		return;
	}
	syscall.Fcntl(fd, syscall.F_SETFL, flags & ^syscall.O_NONBLOCK);
}

type pollServer struct {
	// ... 既存のフィールド ...
	deadline int64;	// next deadline (nsec since 1970)
}

func (s *pollServer) Now() int64 {
	sec, nsec, err := os.Time();
	if err != nil {
		panic("net: os.Time: ", err.String());
	}
	nsec += sec * 1e9;
	return nsec;
}

func (s *pollServer) CheckDeadlines() {
	now := s.Now();
	// ... 期限切れのFDをチェックし、WakeFDを呼び出すロジック ...
}

func (s *pollServer) Run() {
	var scratch [100]byte;
	for {
		var t = s.deadline;
		if t > 0 {
			t = t - s.Now();
			if t < 0 {
				s.CheckDeadlines();
				continue;
			}
		}
		fd, mode, err := s.poll.WaitFD(t); // タイムアウト引数を渡す
		// ... イベント処理ロジック ...
	}
}

func (fd *netFD) Read(p []byte) (n int, err *os.Error) {
	if fd == nil || fd.osfd == nil {
		return -1, os.EINVAL
	}
	fd.rio.Lock(); // 読み込みロック
	defer fd.rio.Unlock(); // 読み込みアンロック
	if fd.rdeadline_delta > 0 {
		fd.rdeadline = pollserver.Now() + fd.rdeadline_delta;
	} else {
		fd.rdeadline = 0;
	}
	n, err = fd.osfd.Read(p);
	for err == os.EAGAIN && fd.rdeadline >= 0 { // タイムアウトチェックを追加
		pollserver.WaitRead(fd);
		n, err = fd.osfd.Read(p)
	}
	return n, err
}

func (fd *netFD) Write(p []byte) (n int, err *os.Error) {
	if fd == nil || fd.osfd == nil {
		return -1, os.EINVAL
	}
	fd.wio.Lock(); // 書き込みロック
	defer fd.wio.Unlock(); // 書き込みアンロック
	if fd.wdeadline_delta > 0 {
		fd.wdeadline = pollserver.Now() + fd.wdeadline_delta;
	} else {
		fd.wdeadline = 0;
	}
	err = nil;
	nn := 0;
	for nn < len(p) {
		n, err = fd.osfd.Write(p[nn:len(p)]);
		if n > 0 {
			nn += n
		}
		if nn == len(p) {
			break;
		}
		if err == os.EAGAIN && fd.wdeadline >= 0 { // タイムアウトチェックを追加
			pollserver.WaitWrite(fd);
			continue;
		}
		if n == 0 || err != nil {
			break;
		}
	}
	return nn, err
}

src/lib/net/net.go

type Conn interface {
	// Read blocks until data is ready from the connection
	// and then reads into b.  It returns the number
	// of bytes read, or 0 if the connection has been closed.
	Read(b []byte) (n int, err *os.Error);

	// Write writes the data in b to the connection.
	Write(b []byte) (n int, err *os.Error);

	// Close closes the connection.
	Close() *os.Error;

	// For packet-based protocols such as UDP,
	// ReadFrom reads the next packet from the network,
	// returning the number of bytes read and the remote
	// address that sent them.
	ReadFrom(b []byte) (n int, addr string, err *os.Error);

	// For packet-based protocols such as UDP,
	// WriteTo writes the byte buffer b to the network
	// as a single payload, sending it to the target address.
	WriteTo(addr string, b []byte) (n int, err *os.Error);

	// SetReadBuffer sets the size of the operating system's
	// receive buffer associated with the connection.
	SetReadBuffer(bytes int) *os.Error;

	// SetReadBuffer sets the size of the operating system's
	// transmit buffer associated with the connection.
	SetWriteBuffer(bytes int) *os.Error;

	// SetTimeout sets the read and write deadlines associated
	// with the connection.
	SetTimeout(nsec int64) *os.Error;

	// SetReadTimeout sets the time (in nanoseconds) that
	// Read will wait for data before returning os.EAGAIN.
	// Setting nsec == 0 (the default) disables the deadline.
	SetReadTimeout(nsec int64) *os.Error;

	// SetWriteTimeout sets the time (in nanoseconds) that
	// Write will wait to send its data before returning os.EAGAIN.
	// Setting nsec == 0 (the default) disables the deadline.
	// Even if write times out, it may return n > 0, indicating that
	// some of the data was successfully written.
	SetWriteTimeout(nsec int64) *os.Error;

	// SetLinger sets the behavior of Close() on a connection
	// which still has data waiting to be sent or to be acknowledged.
	//
	// If sec < 0 (the default), Close returns immediately and
	// the operating system finishes sending the data in the background.
	//
	// If sec == 0, Close returns immediately and the operating system
	// discards any unsent or unacknowledged data.
	//
	// If sec > 0, Close blocks for at most sec seconds waiting for
	// data to be sent and acknowledged.
	SetLinger(sec int) *os.Error;

	// SetReuseAddr sets whether it is okay to reuse addresses
	// from recent connections that were not properly closed.
	SetReuseAddr(reuseaddr bool) *os.Error;

	// SetDontRoute sets whether outgoing messages should
	// bypass the system routing tables.
	SetDontRoute(dontroute bool) *os.Error;

	// SetKeepAlive sets whether the operating system should send
	// keepalive messages on the connection.
	SetKeepAlive(keepalive bool) *os.Error;

	// BindToDevice binds a connection to a particular network device.
	BindToDevice(dev string) *os.Error;
}

コアとなるコードの解説

タイムアウトの実装

GoのネットワークI/Oは、OSのノンブロッキングI/Oとイベント通知メカニズム(kqueueepoll)を組み合わせて実装されています。以前は、SetReadTimeoutSetWriteTimeoutがOSのソケットオプション(SO_RCVTIMEO, SO_SNDTIMEO)を直接設定していましたが、これにはいくつかの制限がありました。例えば、OSによってはミリ秒単位の精度しかなかったり、読み込みと書き込みで異なるタイムアウトを設定するのが難しかったりする場合があります。

このコミットでは、Goランタイム自身がタイムアウトを管理するようになりました。

  • netFD構造体にrdeadline_deltawdeadline_deltaが追加され、これはユーザーが設定したタイムアウト期間(ナノ秒)を保持します。
  • netFD.ReadnetFD.Writeが呼び出されると、pollserver.Now()で現在の時刻を取得し、rdeadline_deltaまたはwdeadline_deltaを加算して、I/O操作の期限時刻(rdeadlineまたはwdeadline)を計算します。
  • I/O操作がos.EAGAIN(ノンブロッキングI/Oでデータがまだ準備できていないことを示す)を返した場合、pollserver.WaitRead(fd)またはpollserver.WaitWrite(fd)が呼び出されます。これらの関数は、内部的にpollServerにI/Oイベントの待機を登録します。
  • pollServerRunメソッドは、pollster.WaitFD(t)を呼び出してOSのイベント通知メカニズムを待機します。ここでtは、現在登録されているすべてのI/O操作の中で最も近い期限時刻までの残り時間です。これにより、OSレベルでタイムアウトを効率的に処理できます。
  • pollServer.CheckDeadlines()は、pollServerのループ内で定期的に実行され、期限切れのI/O操作を特定し、それらのnetFDrdeadlineまたはwdeadline-1に設定します。これにより、netFD.ReadnetFD.Writeのループが終了し、os.EAGAINエラーが返されることで、アプリケーションにタイムアウトが通知されます。

このアプローチにより、GoはOSのソケットオプションに依存することなく、より高精度で柔軟なタイムアウト制御を実現しています。

並行書き込み時のロック

netFD構造体に追加されたsync.Mutex型のriowioは、それぞれ読み込みと書き込み操作を保護するためのものです。

  • netFD.Readメソッドの冒頭でfd.rio.Lock()が呼び出され、読み込み操作が完了するまで他のゴルーチンが同じnetFDに対して読み込みを行うのをブロックします。
  • 同様に、netFD.Writeメソッドの冒頭でfd.wio.Lock()が呼び出され、書き込み操作が完了するまで他のゴルーチンが同じnetFDに対して書き込みを行うのをブロックします。

これにより、複数のゴルーチンが同時に同じネットワーク接続に対して書き込みを行っても、それらの書き込みがインターリーブされることなく、データが正しく送信されることが保証されます。これは、TCPのようなストリーム指向のプロトコルにおいて、アプリケーションレベルでのデータ整合性を保つ上で非常に重要です。

Connインターフェースのドキュメント強化

src/lib/net/net.goにおけるConnインターフェースの各メソッドへの詳細なコメント追加は、Goの標準ライブラリの品質と使いやすさを向上させるための重要なステップです。これらのコメントは、各メソッドの目的、引数、戻り値、および特定の振る舞いについて明確な説明を提供します。これにより、開発者はConnインターフェースをより正確に理解し、適切に使用できるようになります。また、これはGoのドキュメント生成ツールが自動的に高品質なAPIドキュメントを生成するための基盤となります。

関連リンク

  • Go言語の初期のネットワークパッケージに関する議論や設計ドキュメント(当時のGoコミュニティのメーリングリストやデザインドキュメントを検索すると、より深い背景情報が見つかる可能性があります)。
  • kqueueepollに関するOSのドキュメントや解説記事。
  • Go言語のsyncパッケージとミューテックスに関する公式ドキュメント。

参考にした情報源リンク