これを実装するには多くの方法があるため、Windows での潜在的な種類の非同期 (オーバーラップ) I/O 実装について説明したいと思います。Windows のオーバーラップ I/O は、データを非同期的に処理する機能を提供します。つまり、操作の実行はノンブロッキングです。
編集:この質問の目的は、一方では私自身の実装の改善について議論し、他方では代替実装について議論することです。どの非同期 I/O 実装が並列の重い I/O で最も理にかなっており、ほとんどがシングル スレッドの小さなアプリケーションで最も理にかなっています。
MSDNを引用します:
関数が同期的に実行されると、操作が完了するまで戻りません。これは、時間のかかる操作が終了するのを待っている間、呼び出し元のスレッドの実行が無期限にブロックされる可能性があることを意味します。オーバーラップ操作のために呼び出された関数は、操作が完了していなくても、すぐに戻ることができます。これにより、時間のかかる I/O 操作をバックグラウンドで実行できるようになり、呼び出し元のスレッドは自由に他のタスクを実行できます。たとえば、単一のスレッドは、異なるハンドルで同時 I/O 操作を実行したり、同じハンドルで同時の読み取り操作と書き込み操作を実行したりすることもできます。
読者はオーバーラップ I/O の基本概念に精通していることを前提としています。
非同期 I/O のもう 1 つの解決策は完了ポートですが、これはこの説明の主題ではありません。その他の I/O 概念の詳細については、MSDN の「ファイル管理について > 入力および出力 (I/O) > I/O の概念」を参照してください。
ここで私の (C/C++) 実装を紹介し、議論のために共有したいと思います。
これは、拡張された OVERLAPPED 構造体で、次のように呼ばれIoOperation
ます。
struct IoOperation : OVERLAPPED {
HANDLE Handle;
unsigned int Operation;
char* Buffer;
unsigned int BufferSize;
}
ReadFile
この構造体は、 orのような非同期操作WriteFile
が呼び出されるたびに作成されます。フィールドは、Handle
対応するデバイス/ファイル ハンドルで初期化されます。Operation
どの操作が呼び出されたかを示すユーザー定義フィールドです。フィールドBuffer
は、指定された size で以前に割り当てられたメモリのチャンクへのポインタBufferSize
です。もちろん、この構造体は自由に展開できます。操作結果、実際に転送されたサイズなどを含めることができます。
最初に必要なのは、重複した I/O が完了するたびに通知される (自動リセット) イベント ハンドルです。
HANDLE hEvent = CreateEvent(0, FALSE, FALSE, 0);
最初に、すべての非同期操作に対して 1 つのイベントのみを使用することにしました。次に、このイベントを RegisterWaitForSingleObject を使用してスレッド プール スレッドに登録することにしました。
HANDLE hWait = 0;
....
RegisterWaitForSingleObject(
&hWait,
hEvent,
WaitOrTimerCallback,
this,
INFINITE,
WT_EXECUTEINPERSISTENTTHREAD | WT_EXECUTELONGFUNCTION
);
したがって、このイベントが通知されるたびに、コールバックWaitOrTimerCallback
が呼び出されます。
非同期操作は次のように初期化されます。
IoOperation* Io = new IoOperation(hFile, hEvent, IoOperation::Write, Data, DataSize);
if (IoQueue->Enqueue(Io)) {
WriteFile(hFile, Io->Buffer, Io->BufferSize, 0, Io);
}
各操作はキューに入れられGetOverlappedResult
、コールバックでの呼び出しが成功した後に削除されWaitOrTimerCallback
ます。new
ここで常に呼び出す代わりに、メモリプールを使用してメモリの断片化を回避し、割り当てを高速化できます。
VOID CALLBACK WaitOrTimerCallback(PVOID Parameter, BOOLEAN TimerOrWaitFired) {
list<IoOperation*>::iterator it = IoQueue.begin();
while (it != IoQueue.end()) {
bool IsComplete = true;
DWORD Transfered = 0;
IoOperation* Io = *it;
if (GetOverlappedResult(Io->Handle, Io, &Transfered, FALSE)) {
if (Io->Operation == IoOperation::Read) {
// Handle Read, virtual OnRead(), SetEvent, etc.
} else if (Io->Operation == IoOperation::Write) {
// Handle Read, virtual OnWrite(), SetEvent, etc.
} else {
// ...
}
} else {
if (GetLastError() == ERROR_IO_INCOMPLETE) {
IsComplete = false;
} else {
// Handle Error
}
}
if (IsComplete) {
delete Io;
it = IoQueue.erase(it);
} else {
it++;
}
}
}
もちろん、マルチスレッドを安全に行うには、たとえば I/O キューにアクセスするときにロック保護 (クリティカル セクション) が必要です。
この種の実装には利点もありますが、欠点もあります。
利点:
- 永続的なスレッド プール スレッドでの実行。手動でスレッドを作成する必要はありません
- 必要なイベントは 1 つだけです
- 各操作は I/O キューに入れられます (CancelIoEx は後で呼び出すことができます)
短所:
- I/O キューには余分なメモリ/CPU 時間が必要です
- GetOverlappedResult は、キューに入れられたすべての I/O に対して、未完了のものも含めて呼び出されます