非同期ディスク I/O が Windows で同期として表示される
この記事は、I/O の既定の動作は同期的ですが、非同期として表示される問題を解決するのに役立ちます。
元の製品バージョン: Windows
元の KB 番号: 156932
まとめ
Microsoft Windows のファイル I/O は、同期または非同期にすることができます。 I/O の既定の動作は同期であり、I/O 関数が呼び出され、I/O が完了すると返されます。 非同期 I/O を使用すると、I/O 関数はすぐに呼び出し元に実行を戻すことができますが、I/O は将来完了するとは見なされません。 オペレーティング システムは、I/O が完了すると呼び出し元に通知します。 代わりに、呼び出し元は、オペレーティング システムのサービスを使用して、未処理の I/O 操作の状態を確認できます。
非同期 I/O の利点は、I/O 操作の完了中に呼び出し元が他の作業を実行したり、より多くの要求を発行したりする時間があるということです。 重複 I/O という用語は、非同期 I/O では頻繁に使用され、同期 I/O では重複しない I/O が使用されます。 この記事では、I/O 操作の非同期操作と同期操作という用語を使用します。 この記事では、 CreateFile
、 ReadFile
、 WriteFile
などのファイル I/O 関数に精通していることを前提としています。
多くの場合、非同期 I/O 操作は同期 I/O と同じように動作します。 この記事で後のセクションで説明する特定の条件により、I/O 操作が同期的に完了します。 I/O 関数は I/O が完了するまで戻らないので、呼び出し元はバックグラウンド処理の時間がありません。
いくつかの関数は、同期および非同期 I/O に関連しています。 この記事では、例として ReadFile
と WriteFile
を使用します。 良い選択肢は、 ReadFileEx
と WriteFileEx
です。 この記事ではディスク I/O のみを具体的に説明しますが、その原則の多くは、シリアル I/O やネットワーク I/O などの他の種類の I/O に適用できます。
非同期 I/O を設定する
FILE_FLAG_OVERLAPPED
フラグは、ファイルを開くときにCreateFile
で指定する必要があります。 このフラグを使用すると、ファイルに対する I/O 操作を非同期的に実行できます。 例を次に示します。
HANDLE hFile;
hFile = CreateFile(szFileName,
GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
ErrorOpeningFile();
非同期 I/O のコードを作成する場合は、必要に応じて操作を同期する権限がシステムによって確保されるため、注意してください。 そのため、プログラムを記述して、同期的または非同期的に完了する可能性のある I/O 操作を正しく処理することをお勧めします。 サンプル コードでは、この考慮事項を示します。
非同期操作の完了を待機している間にプログラムで実行できることは多数あります。たとえば、追加の操作をキューに入れる、バックグラウンドで作業を行うなどです。 たとえば、次のコードは、読み取り操作の重複した完了と重複しない完了を適切に処理します。 未処理の I/O が完了するまで待機する以外に何も行いません。
if (!ReadFile(hFile,
pDataBuf,
dwSizeOfBuffer,
&NumberOfBytesRead,
&osReadOperation )
{
if (GetLastError() != ERROR_IO_PENDING)
{
// Some other error occurred while reading the file.
ErrorReadingFile();
ExitProcess(0);
}
else
// Operation has been queued and
// will complete in the future.
fOverlapped = TRUE;
}
else
// Operation has completed immediately.
fOverlapped = FALSE;
if (fOverlapped)
{
// Wait for the operation to complete before continuing.
// You could do some background work if you wanted to.
if (GetOverlappedResult( hFile,
&osReadOperation,
&NumberOfBytesTransferred,
TRUE))
ReadHasCompleted(NumberOfBytesTransferred);
else
// Operation has completed, but it failed.
ErrorReadingFile();
}
else
ReadHasCompleted(NumberOfBytesRead);
Note
&NumberOfBytesRead
ReadFile
に渡される&NumberOfBytesTransferred
は、GetOverlappedResult
に渡されるのとは異なります。 操作が非同期に行われた場合、 GetOverlappedResult
を使用して、操作が完了した後に転送された実際のバイト数が決定されます。 ReadFile
に渡される&NumberOfBytesRead
は意味がありません。
一方、操作が直ちに完了した場合、 &NumberOfBytesRead
読み取られたバイト数に対して有効 ReadFile
に渡されます。 この場合、ReadFile
に渡されるOVERLAPPED
構造体は無視します。GetOverlappedResult
やWaitForSingleObject
では使用しないでください。
非同期操作に関するもう 1 つの注意事項は、保留中の操作が完了するまで OVERLAPPED
構造体を使用してはならないということです。 つまり、3 つの未処理の I/O 操作がある場合は、3 つの OVERLAPPED
構造体を使用する必要があります。 OVERLAPPED
構造体を再利用すると、I/O 操作で予期しない結果が発生し、データの破損が発生する可能性があります。 さらに、 OVERLAPPED
構造体を初めて使用する前、または以前の操作が完了した後に再利用する前に、残りのデータが新しい操作に影響を与えないように、正しく初期化する必要があります。
操作で使用されるデータ バッファーにも、同じ種類の制限が適用されます。 対応する I/O 操作が完了するまで、データ バッファーを読み取ったり書き込んだりすることはできません。バッファーの読み取りまたは書き込みを行うと、エラーや破損したデータが発生する可能性があります。
非同期 I/O は引き続き同期的に表示される
ただし、この記事の前の手順に従った場合、通常、すべての I/O 操作は発行された順序で同期的に完了し、ReadFile
操作のいずれも FALSE を返しませんGetLastError()
ERROR_IO_PENDING
が返されます。つまり、バックグラウンドでの作業を行う時間がありません。 これが発生するのはなぜですか?
非同期操作用にコーディングした場合でも、I/O 操作が同期的に完了する理由は多数あります。
Compression
非同期操作の 1 つの障害は、新しいテクノロジ ファイル システム (NTFS) 圧縮です。 ファイル システム ドライバーは、圧縮ファイルに非同期的にアクセスしません。代わりに、すべての操作が同期的になります。 この問題は、COMPRESS や PKZIP のようなユーティリティで圧縮されたファイルには適用されません。
NTFS 暗号化
圧縮と同様に、ファイル暗号化により、システム ドライバーは非同期 I/O を同期に変換します。 ファイルが復号化されると、I/O 要求は非同期になります。
ファイルを拡張する
I/O 操作が同期的に完了するもう 1 つの理由は、操作自体です。 Windows では、長さを拡張するファイルへの書き込み操作は同期的になります。
Note
アプリケーションは、 SetFileValidData
関数を使用してファイルの有効なデータ長を変更し、 WriteFile
を発行することで、前述の書き込み操作を非同期にすることができます。
SetFileValidData
(Windows XP 以降のバージョンで利用可能) を使用すると、アプリケーションはファイルをゼロフィルするパフォーマンスの低下を招くことなく、効率的にファイルを拡張できます。
NTFS ファイル システムは、 SetFileValidData
で定義されている有効なデータ長 (VDL) までのデータをゼロフィルしないため、この関数は、以前に他のファイルによって占有されていたクラスターにファイルが割り当てられる可能性があるセキュリティ上の影響を与えます。 そのため、 SetFileValidData
は、呼び出し元が新しい SeManageVolumePrivilege
を有効にしている必要があります (既定では、これは管理者にのみ割り当てられます)。 Microsoft では、独立系ソフトウェア ベンダー (ISV) は、このような機能を使用することの影響を慎重に検討することをお勧めします。
キャッシュ
ほとんどの I/O ドライバー (ディスク、通信など) には特殊なケース コードがあり、I/O 要求をすぐに完了できる場合、操作は完了し、 ReadFile
または WriteFile
関数は TRUE を返します。 あらゆる点で、これらの種類の操作は同期的に見えます。 ディスク デバイスの場合、通常、データがメモリにキャッシュされるとすぐに I/O 要求を完了できます。
データがキャッシュにありません
ただし、データがキャッシュ内にない場合は、キャッシュ スキームが動作する可能性があります。 Windows キャッシュは、ファイル マッピングを使用して内部的に実装されます。 Windows のメモリ マネージャーには、キャッシュ マネージャーによって使用されるファイル マッピングを管理するための非同期ページ フォールト メカニズムが用意されていません。 キャッシュ マネージャーは、要求されたページがメモリ内にあるかどうかを確認できます。そのため、非同期キャッシュ読み取りを発行し、ページがメモリ内にない場合、ファイル システム ドライバーはスレッドをブロックしたくないと想定し、要求はワーカー スレッドの限られたプールによって処理されます。 読み取りがまだ保留中の ReadFile
呼び出しの後、制御がプログラムに返されます。
これは少数の要求では問題ありませんが、ワーカー スレッドのプールは制限されているため (現在、16 MB のシステムでは 3 つ)、特定の時点でディスク ドライバーにキューに入れられている要求はごくわずかです。 キャッシュ内にないデータに対して多数の I/O 操作を発行すると、キャッシュ マネージャーとメモリ マネージャーが飽和状態になり、要求が同期されます。
キャッシュ マネージャーの動作は、ファイルに順番にアクセスするかランダムにアクセスするかによっても影響を受けます。 キャッシュの利点は、ファイルに順番にアクセスする場合に最も多く見られます。 CreateFile
呼び出しのFILE_FLAG_SEQUENTIAL_SCAN
フラグは、この種類のアクセス用にキャッシュを最適化します。 ただし、ランダムな方法でファイルにアクセスする場合は、CreateFile
のFILE_FLAG_RANDOM_ACCESS
フラグを使用して、ランダム アクセスの動作を最適化するようにキャッシュ マネージャーに指示します。
キャッシュを使用しない
FILE_FLAG_NO_BUFFERING
フラグは、非同期操作のファイル システムの動作に最も影響します。 I/O 要求が非同期であることを保証する最善の方法です。 キャッシュ メカニズムをまったく使用しないようにファイル システムに指示します。
Note
このフラグの使用には、データ バッファーの配置とデバイスのセクター サイズに関連するいくつかの制限があります。 詳細については、このフラグの適切な使用に関する CreateFile 関数のドキュメントの関数リファレンスを参照してください。
実際のテスト結果
サンプル コードのテスト結果を次に示します。 数値の大きさはここでは重要ではなく、コンピューターによって異なりますが、数値の関係は、パフォーマンスに対するフラグの一般的な影響を明るくします。
次のいずれかの結果が表示されます。
テスト 1
Asynchronous, unbuffered I/O: asynchio /f*.dat /n Operations completed out of the order in which they were requested. 500 requests queued in 0.224264 second. 500 requests completed in 4.982481 seconds.
このテストは、前述のプログラムが 500 個の I/O 要求を迅速に発行し、他の作業を実行したり、より多くの要求を発行したりする時間が多かったことを示しています。
テスト 2
Synchronous, unbuffered I/O: asynchio /f*.dat /s /n Operations completed in the order issued. 500 requests queued and completed in 4.495806 seconds.
このテストは、このプログラムが ReadFile を呼び出して操作を完了するのに 4.495880 秒を費やしたが、テスト 1 が同じ要求を発行するのに 0.224264 秒しか費やさしなかったことを示しています。 テスト 2 では、プログラムがバックグラウンド作業を行う余分な時間はありませんでした。
テスト 3
Asynchronous, buffered I/O: asynchio /f*.dat Operations completed in the order issued. 500 requests issued and completed in 0.251670 second.
このテストでは、キャッシュの同期的な性質を示します。 すべての読み取りは 0.251670 秒で発行され、完了しました。 つまり、非同期要求は同期的に完了しました。 このテストでは、データがキャッシュ内にある場合のキャッシュ マネージャーの高パフォーマンスも示します。
テスト 4
Synchronous, buffered I/O: asynchio /f*.dat /s Operations completed in the order issued. 500 requests and completed in 0.217011 seconds.
このテストは、テスト 3 と同じ結果を示します。 キャッシュからの同期読み取りは、キャッシュからの非同期読み取りよりも少し高速に完了します。 このテストでは、データがキャッシュ内にある場合のキャッシュ マネージャーの高パフォーマンスも示します。
まとめ
どの方法が最適かは、プログラムが実行する操作の種類、サイズ、および数によってすべて異なるためです。
CreateFile
する特別なフラグを指定しない既定のファイル アクセスは、同期操作とキャッシュ操作です。
Note
ファイル システム ドライバーは、変更されたデータの予測的な非同期先行書き込みと非同期遅延書き込みを行うため、このモードでは自動非同期動作が発生します。 この動作ではアプリケーションの I/O は非同期になりませんが、ほとんどの単純なアプリケーションにとって理想的なケースです。
一方、アプリケーションが単純でない場合は、この記事で前に示したテストと同様に、プロファイリングとパフォーマンスの監視を実行して最適な方法を決定する必要があります。 ReadFile
またはWriteFile
関数で費やされた時間をプロファイリングし、この時間を実際の I/O 操作が完了するまでにかかる時間と比較すると役立ちます。 ほとんどの時間が実際に I/O の発行に費やされた場合、I/O は同期的に完了します。 ただし、I/O 要求の発行に費やされた時間が、I/O 操作の完了にかかる時間と比較して比較的少ない場合、操作は非同期的に処理されます。 この記事で前述したサンプル コードでは、 QueryPerformanceCounter
関数を使用して独自の内部プロファイリングを実行します。
パフォーマンス監視は、プログラムがディスクとキャッシュを使用している効率を判断するのに役立ちます。 Cache オブジェクトのパフォーマンス カウンターを追跡すると、キャッシュ マネージャーのパフォーマンスが示されます。 物理ディスクまたは論理ディスク オブジェクトのパフォーマンス カウンターを追跡すると、ディスク システムのパフォーマンスが示されます。
パフォーマンスの監視に役立つユーティリティがいくつかあります。 PerfMon
と DiskPerf
は特に便利です。 システムがディスク・システムのパフォーマンスに関するデータを収集するには、最初に DiskPerf
コマンドを発行する必要があります。 コマンドを発行した後、システムを再起動してデータ収集を開始する必要があります。