同期I/O・非同期I/O・OVERLAPPEDの知識マップ(知識グラフ)
同期I/O
I/O要求の完了まで呼び出しが戻らないI/Oの形態。実体は「完了する前には戻らない」という保証であり、要求がペンディングになったときだけスレッドが待機状態になる。
非同期I/O
要求を発行した呼び出しがI/Oの完了を待たずにすぐ戻り、完了を別途の通知(イベント・APC・I/O完了ポートなど)で受け取るI/Oの形態。
オーバーラップI/O
Overlapped I/O
FILE_FLAG_OVERLAPPEDを付けて開いたハンドルに対し、操作ごとのOVERLAPPED構造体を渡して行うWin32の非同期I/Oの具体的な実装形態。モードはハンドル単位でCreateFileの時点に決まる。
オーバーラップドI/O
非同期I/O(Win32)
CreateFile
ファイルだけでなく物理ディスク・ボリューム・COMポート・パイプなどのデバイスも、名前解決を通じて共通に開けるWin32 API。開いた対象ごとにファイルオブジェクトが作られる。
OVERLAPPED構造体
発行中の非同期I/O操作1つを識別し、その状態(読み書き位置・完了イベント・転送バイト数)を運ぶデータ構造。同時に発行する操作の数だけ必要で、使い回すとデータ破損につながる。
I/Oマネージャー
Windowsカーネルモードで、アプリケーションとデバイスドライバーの間のI/O要求を統括するコンポーネント。要求のほとんどをIRPに詰めてデバイススタックへ流す。
IOマネージャー
ReadFile/WriteFile
ファイルやデバイスの読み書きを行うWin32 API。非同期モードのハンドルに対してはOVERLAPPED構造体の指定が必須で、戻り値とGetLastErrorの組み合わせで結果が3通りに分かれる。
同期完了
非同期モードのハンドルへ発行したI/Oが、呼び出しの中でその場(TRUEを返して)完了すること。キャッシュヒット・NTFS圧縮/暗号化・拡張書き込みなどで起こる。
キャッシュマネージャー
ファイルデータをメモリ上のシステムキャッシュに保持し、読み書きの多くをディスクI/Oなしで処理させるWindowsカーネルのコンポーネント。ファストI/Oの近道もこのキャッシュを直接利用する。
NTFS圧縮
データを圧縮ユニット単位で圧縮して格納するNTFSの透過的な圧縮機能。読み書きのたびに展開・再圧縮のコストが伴う。
ファイル圧縮(NTFS)
NTFS暗号化(EFS)ファイル
EFS(Encrypting File System)で暗号化されたファイル。NTFS圧縮と同様、アクセスが同期処理に変換されるため非同期発行でも同期的に完了する。
ファイルを伸ばす書き込み
ファイルの長さを増やす書き込み操作。非同期発行でも同期的に完了する条件の1つ。
ERROR_IO_PENDING
非同期モードのハンドルへの発行が受理され、完了はまだであることを示すエラーコード(997)。エラーではなく「受理」を意味する。
GetOverlappedResult
非同期操作の結果(成否と転送バイト数)を、対応するOVERLAPPED構造体から取得するWin32 API。bWait=TRUEで完了まで待つこともできる。
ReadFileEx/WriteFileEx
完了時に呼ばれる完了ルーチン(コールバック)を受け取るReadFile/WriteFileの拡張版。完了ルーチンは発行したスレッドのAPCキューに積まれる。
APC(非同期プロシージャコール)
特定スレッドのコンテキストで非同期に実行される関数。各スレッドが自分のAPCキューを持ち、ユーザーモードAPCはスレッドがalertable状態のときにのみ実行される。
Alertable wait
SleepEx・WaitForSingleObjectExなど、待機中にAPCキューのエントリを実行できる待機状態。APCが配達されるにはスレッドがこの状態に入っている必要がある。
alertable待機
CancelIoEx
どのスレッドが発行したかに関係なく、指定ハンドルの未完了I/Oにキャンセルを要求できるWin32 API。取り消された操作はERROR_OPERATION_ABORTEDでの完了として返る。
CancelIo
呼び出したスレッド自身が発行したI/O操作しか取り消せない、CancelIoExの前身にあたるWin32 API。
ERROR_OPERATION_ABORTED
CancelIoEx等で取り消された操作が、完了として返ってくる際のエラーコード。この通知を受け取るまでOVERLAPPEDとバッファを解放してはならない。
CancelSynchronousIo
指定したスレッドが実行中の同期I/O操作にキャンセルを要求するWin32 API。同期I/Oで固まっている別スレッドを外から取り消すために使う。
SetFileCompletionNotificationModes
I/Oが即時に成功した場合にI/O完了ポートへ完了パケットを積まない動作(FILE_SKIP_COMPLETION_PORT_ON_SUCCESS)を選べるWin32 API。OVERLAPPED.hEventのシグナルは抑止しない。
I/O完了ポート(IOCP)
多数の非同期I/Oの完了通知を1つのFIFOキューに集約しつつ、実行可能なスレッド数をコンカレンシー値以下に保つスケジューリング機構を兼ねる、Windowsカーネルの仕組み。
FileOptions.Asynchronous
.NETのFileStreamコンストラクターやFile.OpenHandleで、OSレベルの非同期I/O(FILE_FLAG_OVERLAPPED)を有効にする指定。ハンドルのモードを直接決める。
見せかけの非同期
同期モードのハンドルに対してReadAsync/WriteAsyncを呼んだ結果生じる状態。呼び出し元はブロックされないが、裏でスレッドプールのスレッドが同期読み書きを肩代わりして待つ。
File.OpenHandle + RandomAccess
.NET 6以降で、SafeFileHandleとオフセットを明示して読み書きするAPI。Win32の非同期ハンドル+OVERLAPPED.Offsetという素の形に近い書き方ができる。
IRP(I/O要求パケット)
デバイスドライバーに送られる要求のほとんどが詰め込まれるパケット。要求全体のヘッダーと、経由するドライバーの数だけ並ぶI/Oスタックロケーションを持つ。
CancellationToken(.NET)
.NETの協調的キャンセルの仕組み。非同期モードのハンドルへのファイルI/Oでは、内部的にCancelIoExの呼び出しに行き着く。
Overlapped I/Oは、FILE_FLAG_OVERLAPPEDとOVERLAPPED構造体という具体的なAPIの形で非同期I/Oを実装したもの
非同期モードはCreateFileにFILE_FLAG_OVERLAPPEDを渡すことでハンドル単位に決まり、呼び出しごとには切り替えられない
発行中の操作1つを識別し、その状態(位置・完了・転送バイト数)を運ぶためにOVERLAPPED構造体を使う
要求がペンディングになった場合、I/Oマネージャーが完了を待ち合わせてからアプリへ制御を返すことで同期I/Oを実現する
同じハンドルは同期モードか非同期モードのどちらか一方であり、呼び出しごとに切り替えることはできない
非同期モードのハンドルにReadFile/WriteFileを発行する場合、OVERLAPPED構造体の指定が必須になる
非同期ハンドルへの発行でも、要求がすぐ満たせる場合はTRUEを返してその場で完了することがある
同期完了の代表例は、要求されたデータがキャッシュマネージャーの管理するキャッシュに全部載っている場合である
NTFS圧縮されたファイルへのアクセスは、ファイルシステムドライバーが同期処理に変換するため、非同期発行でも同期的に完了する
NTFS暗号化(EFS)されたファイルへのアクセスも、同期処理に変換されるため非同期発行でも同期的に完了する
ファイルの長さを伸ばす書き込みは同期処理になるため、非同期発行でも同期的に完了する
1回の発行結果として、TRUE(同期完了)とFALSE+ERROR_IO_PENDING(進行中)は同時には起こらない
要求が受理されたが完了はまだの場合、FALSEとERROR_IO_PENDINGが返り、完了は通知側で受け取る
ERROR_IO_PENDINGは非同期モード(Overlapped I/O)のハンドルへの発行に特有の戻り値である
GetOverlappedResultは、対応するOVERLAPPED構造体から操作の成否と転送バイト数を取り出す
ReadFileEx/WriteFileExは、完了時に呼ばれる完了ルーチンを発行したスレッドのAPCキューに積む方式で通知する
APCが実際に実行されるのは、対象スレッドがSleepExなどのalertable waitに入ったときに限られる
CancelIoExはCancelIoの後継で、発行したスレッド以外からも未完了I/Oを取り消せるようになった(Vistaで追加)
特定の1操作だけを取り消したい場合、対応するOVERLAPPEDへのポインターを渡す(NULLならそのハンドルの全操作が対象)
取り消しに成功した操作はERROR_OPERATION_ABORTEDとしての完了通知になる。完了間際だった操作は正常完了することもある
ERROR_OPERATION_ABORTEDの完了通知を受け取るまでは、OVERLAPPEDとバッファをカーネルが使用中であり解放してはならない
同期I/Oで固まっている別スレッドを外から取り消したいときは、CancelIoEx系ではなくCancelSynchronousIoを使う
非同期I/Oの取り消しには、どのスレッドが発行したかに関係なく使えるCancelIoExを使う
CancelIoは呼び出したスレッド自身が発行した操作しか取り消せない制約があり、CancelIoExが使える環境であえて選ぶ理由はない
FILE_SKIP_COMPLETION_PORT_ON_SUCCESSが抑止できるのはI/O完了ポートへの完了パケットだけで、OVERLAPPED.hEventのシグナルは抑止されない
同期完了時に結果はその場で分かっているので、高頻度I/Oでは完了ポートへのパケットを省いてキュー経由の無駄を減らす最適化として使う
.NETのFileOptions.Asynchronous(useAsync)はFILE_FLAG_OVERLAPPEDへの直通スイッチで、指定するとハンドルが非同期モードで開かれる
見せかけの非同期は、ハンドルが同期モードのままReadAsync/WriteAsyncを呼んだときに生じる。スレッドプールのスレッドが同期読み書きを肩代わりして待つ
FileOptions.Asynchronousでハンドルを非同期モードで開けば、ReadAsync呼び出しが本物の非同期I/Oになり、見せかけの非同期を避けられる
.NET 6以降のFile.OpenHandle+RandomAccessを非同期に使うには、FileOptions.Asynchronousでハンドルを開く
CancelSynchronousIoで取り消された同期操作は、ERROR_OPERATION_ABORTEDでの失敗として返る
多数の同時I/Oを少数のスレッドで受ける完了通知の経路として、ハンドルをI/O完了ポートに関連付けることができる
非同期に発行しても、キャッシュヒットやNTFS圧縮・暗号化・拡張書き込みなどの条件が揃えばその場で完了することがあり、応答性は保証されない
CancelIoExは該当する未完了IRPにキャンセルをマークし、ドライバーのキャンセルルーチンを呼び出させる
非同期モードのハンドルであれば、.NETのCancellationTokenによるファイルI/Oのキャンセルは内部的にCancelIoExに行き着く
同期モードのハンドルによる見せかけの非同期では、取り消し対象となるオーバーラップ操作が存在しないため、CancellationTokenによる中断は確実には効かない