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

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

このコミットは、Go言語のnetパッケージにおけるWindows環境でのI/O処理に関する重要な改善を導入しています。具体的には、読み込み(Read)と書き込み(Write)の処理をそれぞれ専用のゴルーチン(OSスレッドにロックされたもの)で実行するように変更し、Windows上でのネットワークI/Oの効率と安定性を向上させています。

コミット

commit 11320fa500c7201065baf1958e237ce4a03b3030
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Tue Aug 27 14:53:57 2013 +1000

    net: have separate read and write processing threads on windows
    
    Fixes #4195
    
    R=golang-dev, mikioh.mikioh
    CC=golang-dev
    https://golang.org/cl/12960046

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

https://github.com/golang/go/commit/11320fa500c7201065baf1958e237ce4a03b3030

元コミット内容

net: have separate read and write processing threads on windows

Fixes #4195

R=golang-dev, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/12960046

変更の背景

このコミットは、Go言語のIssue #4195「net: CancelIoEx is not available on XP」を修正するために行われました。このIssueは、Windows XPのような古いOS環境において、CancelIoExという非同期I/O操作をキャンセルするためのAPIが利用できないことに起因する問題に対処しています。

Goのネットワークパッケージは、Windows上で非同期I/O(Overlapped I/O)を利用して高性能なネットワーク通信を実現しています。非同期I/Oでは、I/O操作がバックグラウンドで実行され、完了時に通知されます。しかし、I/O操作をキャンセルする必要がある場合、CancelIoExが利用できない環境では、I/O操作が完了するまで待機するか、あるいはI/O操作がブロックされたままになる可能性がありました。

特に、Goのランタイムは、I/O操作を処理するためにOSスレッドにロックされたゴルーチンを使用します。以前の実装では、読み込みと書き込みの両方のI/O操作を単一のioSrv(I/Oサービス)ゴルーチンが処理していました。この単一のゴルーチンがI/O操作の開始とキャンセルを両方担当していたため、特定の状況下でデッドロックやパフォーマンスの問題が発生する可能性がありました。例えば、あるI/O操作がブロックされている間に、別のI/O操作のキャンセル要求が同じゴルーチンに送られると、処理が滞る可能性がありました。

このコミットの目的は、Windows XPのような環境でもI/O操作のキャンセルが適切に機能し、かつI/O処理の堅牢性と効率を向上させるために、読み込みと書き込みのI/O処理を分離することでした。

前提知識の解説

Go言語のネットワークパッケージ (net)

Go言語のnetパッケージは、TCP/IP、UDP、Unixドメインソケットなどのネットワークプログラミング機能を提供します。Goの標準ライブラリの一部であり、クロスプラットフォームで動作するように設計されています。内部的には、各OSのネイティブなI/Oメカニズム(Linux/Unixではepoll/kqueue、WindowsではI/O Completion Ports (IOCP))を利用して、高効率な非同期I/Oを実現しています。

Windowsの非同期I/O (Overlapped I/O) と I/O Completion Ports (IOCP)

Windowsでは、Overlapped I/Oというメカニズムを使用して非同期I/Oを実行します。これは、I/O操作が完了するのを待たずに、アプリケーションが他の処理を続行できるようにするものです。I/O操作の完了は、イベントオブジェクト、完了ルーチン、またはI/O Completion Ports (IOCP) を通じて通知されます。

  • I/O Completion Ports (IOCP): IOCPは、Windowsにおける高効率な非同期I/O処理のためのメカニズムです。複数のI/O操作の完了通知を効率的に処理するために設計されており、特に多数の同時接続を扱うサーバーアプリケーションでその真価を発揮します。IOCPを使用すると、I/O操作が完了したときに、システムがスレッドプールから利用可能なスレッドを選択し、そのスレッドで完了ルーチンを実行します。これにより、スレッドの数を最小限に抑えつつ、高い並行性を実現できます。

CancelIoCancelIoEx

Windows APIには、発行されたI/O操作をキャンセルするための関数がいくつかあります。

  • CancelIo: 指定されたファイルハンドルに対して発行された保留中のI/O操作をすべてキャンセルします。この関数は、I/O操作を発行したスレッドと同じスレッドから呼び出す必要があります。
  • CancelIoEx: 指定されたファイルハンドルに対して発行された保留中のI/O操作をキャンセルします。CancelIoとは異なり、この関数はI/O操作を発行したスレッドとは異なるスレッドから呼び出すことができます。しかし、CancelIoExはWindows Vista以降で導入されたAPIであり、Windows XPのような古いOSでは利用できません。

OSスレッドにロックされたゴルーチン

Goランタイムでは、特定のゴルーチンをOSスレッドに「ロック」することができます。これは、runtime.LockOSThread()関数を使用して行われます。これにより、そのゴルーチンは常に同じOSスレッド上で実行されることが保証されます。ネットワークI/Oのようなシステムコールを伴う処理や、特定のOSスレッドのコンテキストを必要とする処理(例えば、WindowsのCancelIoのように、I/Oを発行したスレッドと同じスレッドからキャンセルを呼び出す必要がある場合)で利用されます。

技術的詳細

このコミットの核心は、Windows環境におけるGoのnetパッケージのI/O処理を、単一のioSrv(I/Oサービス)ゴルーチンから、読み込み専用のrsrvと書き込み専用のwsrvという2つの独立したI/Oサービスゴルーチンに分離した点です。

以前の実装では、netFD構造体(ネットワークファイルディスクリプタを表す)のread操作とwrite操作の両方が、グローバルなiosrvインスタンスを通じてExecIOを呼び出していました。iosrvは、canCancelIOfalse(つまりCancelIoExが利用できないWindows XPのような環境)の場合、OSスレッドにロックされた単一のゴルーチンProcessRemoteIOを実行し、すべてのI/Oリクエスト(読み込み、書き込み、接続、受け入れなど)を処理していました。

この単一のゴルーチンがすべてのI/O操作の開始とキャンセルを処理するという設計は、CancelIoExが利用できない環境で問題を引き起こしました。CancelIoはI/O操作を発行したスレッドと同じスレッドから呼び出す必要があるため、もしProcessRemoteIOゴルーチンがI/O操作を発行し、そのI/O操作がブロックされた場合、同じゴルーチンがそのI/O操作をキャンセルしようとしても、自身がブロックされているためにキャンセル処理が進まないというデッドロックのような状況が発生する可能性がありました。

この変更により、読み込み操作(WSARecv, WSARecvFrom, AcceptExなど)はrsrvが、書き込み操作(ConnectEx, WSASend, WSASendto, TransmitFileなど)はwsrvがそれぞれ担当するようになりました。これにより、読み込みI/Oがブロックされても書き込みI/Oの処理が妨げられず、またその逆も同様になります。それぞれのI/Oサービスが独立したOSスレッドにロックされたゴルーチンを持つことで、I/O操作の開始とキャンセルがより堅牢に、かつ並行して行えるようになります。

また、canCancelIOというグローバル変数が削除されました。これは、CancelIoExの利用可能性を示すフラグでしたが、このコミットによってI/O処理のアーキテクチャが変更され、CancelIoExの有無に依存しない、より汎用的なI/Oキャンセルメカニズムが実現されたため、不要になりました。

テストコードの変更も行われています。src/pkg/net/http/transport_test.goでは、time.Sleepの時間が50 * time.Millisecondから400 * time.Millisecondに延長されています。これは、I/O処理の分離によってゴルーチンのクリーンアップに時間がかかるようになったため、テストが安定してパスするように調整されたものと考えられます。src/pkg/net/timeout_test.goからは、canCancelIOに依存するテストスキップロジックが削除されています。

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

src/pkg/net/fd_plan9.go および src/pkg/net/fd_unix.go

  • var canCancelIO = true の行が削除されました。この変数は、Windows固有のI/Oキャンセルメカニズムに関連するものであり、Plan 9やUnix環境では不要なため削除されました。

src/pkg/net/fd_windows.go

  • グローバル変数 iosrv *ioSrvrsrv, wsrv *ioSrv に変更されました。これにより、読み込みと書き込みそれぞれにI/Oサービスインスタンスが用意されます。
  • startServer() 関数内で、iosrvの初期化がrsrvwsrvの初期化に置き換えられました。
    • rsrv = new(ioSrv)
    • wsrv = new(ioSrv)
  • canCancelIOfalseの場合の処理ブロック内で、単一のiosrv.ProcessRemoteIO()ゴルーチン起動が、rsrv.ProcessRemoteIO()wsrv.ProcessRemoteIO()の2つのゴルーチン起動に置き換えられました。
    • rsrv.req = make(chan ioSrvReq)
    • go rsrv.ProcessRemoteIO()
    • wsrv.req = make(chan ioSrvReq)
    • go wsrv.ProcessRemoteIO()
  • connect, Read, ReadFrom, Write, WriteTo, acceptといったI/O操作を行うメソッド内で、iosrv.ExecIOの呼び出しが、それぞれの操作に対応するrsrv.ExecIOまたはwsrv.ExecIOの呼び出しに置き換えられました。
    • ConnectEx (書き込み): iosrv.ExecIO -> wsrv.ExecIO
    • WSARecv (読み込み): iosrv.ExecIO -> rsrv.ExecIO
    • WSARecvFrom (読み込み): iosrv.ExecIO -> rsrv.ExecIO
    • WSASend (書き込み): iosrv.ExecIO -> wsrv.ExecIO
    • WSASendto (書き込み): iosrv.ExecIO -> wsrv.ExecIO
    • AcceptEx (読み込み): iosrv.ExecIO -> rsrv.ExecIO

src/pkg/net/http/transport_test.go

  • TestTransportPersistConnLeakShortBody テスト内で、time.Sleepの時間が50 * time.Millisecondから400 * time.Millisecondに増加しました。

src/pkg/net/sendfile_windows.go

  • sendFile 関数内で、iosrv.ExecIOの呼び出しがwsrv.ExecIOに置き換えられました。
    • TransmitFile (書き込み): iosrv.ExecIO -> wsrv.ExecIO

src/pkg/net/timeout_test.go

  • TestReadWriteDeadline テスト内で、canCancelIO変数の値に基づいてテストをスキップするロジックが削除されました。

コアとなるコードの解説

このコミットの主要な変更は、src/pkg/net/fd_windows.goに集中しています。

以前は、GoのWindowsネットワークI/Oは、ioSrvという単一の構造体と、それに関連するProcessRemoteIOゴルーチンによって管理されていました。このProcessRemoteIOゴルーチンは、canCancelIOfalse(つまりCancelIoExが利用できないWindows XPのような環境)の場合に、OSスレッドにロックされて実行され、すべての非同期I/O操作(読み込み、書き込み、接続、受け入れなど)の開始とキャンセルを処理していました。

このコミットでは、この単一のioSrvを、読み込み専用のrsrvと書き込み専用のwsrvという2つの独立したインスタンスに分割しました。

// Before:
// var iosrv *ioSrv
//
// func startServer() {
// 	iosrv = new(ioSrv)
// 	if !canCancelIO {
// 		iosrv.req = make(chan ioSrvReq)
// 		go iosrv.ProcessRemoteIO()
// 	}
// }

// After:
var rsrv, wsrv *ioSrv
var onceStartServer sync.Once

func startServer() {
	onceStartServer.Do(func() { // Ensure startServer runs only once
		rsrv = new(ioSrv)
		wsrv = new(ioSrv)
		// canCancelIO is removed, so this block always runs if it was previously true
		// or if it was false, it now runs for both rsrv and wsrv.
		// The logic implies that if CancelIoEx is not available, we need dedicated threads.
		// Since canCancelIO is removed, this path is now the default for Windows.
		rsrv.req = make(chan ioSrvReq)
		go rsrv.ProcessRemoteIO() // Dedicated goroutine for read operations
		wsrv.req = make(chan ioSrvReq)
		go wsrv.ProcessRemoteIO() // Dedicated goroutine for write operations
	})
}

startServer()関数は、sync.Onceを使用して一度だけ実行されるように保証されています。この関数内で、rsrvwsrvの2つのioSrvインスタンスが作成されます。そして、それぞれに対してProcessRemoteIO()ゴルーチンが起動されます。これらのゴルーチンは、それぞれが専用のチャネルreqを持ち、OSスレッドにロックされて実行されます。

これにより、例えばfd.Read()が呼び出された際には、rsrv.ExecIO()が使用され、読み込み操作がrsrvの担当するゴルーチンによって処理されます。同様に、fd.Write()が呼び出された際には、wsrv.ExecIO()が使用され、書き込み操作がwsrvの担当するゴルーチンによって処理されます。

// Before (example for Read):
// func (fd *netFD) Read(buf []byte) (int, error) {
// 	// ...
// 	n, err := iosrv.ExecIO(o, "WSARecv", func(o *operation) error {
// 		return syscall.WSARecv(o.fd.sysfd, &o.buf, 1, &o.qty, &o.flags, &o.o, nil)
// 	})
// 	// ...
// }

// After (example for Read):
func (fd *netFD) Read(buf []byte) (int, error) {
	// ...
	n, err := rsrv.ExecIO(o, "WSARecv", func(o *operation) error {
		return syscall.WSARecv(o.fd.sysfd, &o.buf, 1, &o.qty, &o.flags, &o.o, nil)
	})
	// ...
}

// After (example for Write):
func (fd *netFD) Write(buf []byte) (int, error) {
	// ...
	return wsrv.ExecIO(o, "WSASend", func(o *operation) error {
		return syscall.WSASend(o.fd.sysfd, &o.buf, 1, &o.qty, 0, &o.o, nil)
	})
}

この分離により、読み込みと書き込みのI/O処理が互いに独立して実行されるようになり、特にCancelIoExが利用できない環境でのI/Oキャンセルの問題が緩和されます。読み込み操作がブロックされても、書き込み操作のキャンセル要求は別のゴルーチンによって処理されるため、システム全体の応答性が向上します。

canCancelIO変数の削除は、この新しいアーキテクチャがCancelIoExの有無に依存せず、常に2つの専用ゴルーチンを使用するように設計されたことを示しています。これにより、コードの複雑さが軽減され、Windows環境でのI/O処理の堅牢性が向上しました。

関連リンク

参考にした情報源リンク