Windows で非同期ディスク I/O が同期として表示される

この記事は、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 操作に非同期と同期という用語を使用します。 この記事では、リーダーが、 などのCreateFileReadFileWriteFileファイル I/O 関数に精通していることを前提としています。

多くの場合、非同期 I/O 操作は同期 I/O と同じように動作します。 この記事で後のセクションで説明する特定の条件により、I/O 操作が同期的に完了します。 I/O 関数は I/O が完了するまで戻らないので、呼び出し元はバックグラウンドで作業する時間がありません。

同期および非同期の I/O に関連する関数がいくつかあります。 この記事では、 と をWriteFile例として使用ReadFileします。 良い選択肢は ReadFileExWriteFileExです。 この記事ではディスク 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);

注:

&NumberOfBytesReadに渡される のは、 にReadFile渡されるGetOverlappedResultのとは異なります&NumberOfBytesTransferred。 操作が非同期にされた場合は、操作 GetOverlappedResult が完了した後に転送された実際のバイト数を決定するために使用されます。 渡された ReadFile&NumberOfBytesRead意味がありません。

一方、操作がすぐに完了した場合、 &NumberOfBytesRead 渡されたは読み取られた ReadFile バイト数に対して有効です。 この場合は、 にReadFile渡された構造体をOVERLAPPED無視します。または WaitForSingleObjectGetOverlappedResult使用しないでください。

非同期操作に関するもう 1 つの注意点は、保留中の操作が完了するまで構造体を OVERLAPPED 使用してはならないということです。 つまり、未処理の I/O 操作が 3 つある場合は、3 つの OVERLAPPED 構造体を使用する必要があります。 構造体を OVERLAPPED 再利用すると、I/O 操作で予期しない結果が発生し、データの破損が発生する可能性があります。 さらに、構造体を初めて使用 OVERLAPPED する前、または以前の操作が完了した後に再利用する前に、残りのデータが新しい操作に影響を与えないように、正しく初期化する必要があります。

操作で使用されるデータ バッファーにも、同じ種類の制限が適用されます。 データ バッファーは、対応する I/O 操作が完了するまで読み取りまたは書き込みできません。バッファーを読み取ったり書き込んだりすると、エラーや破損したデータが発生する可能性があります。

非同期 I/O は引き続き同期的に表示されます

ただし、この記事の前の手順に従った場合でも、すべての I/O 操作は通常、発行された順序で同期的に完了し、返された FALSEGetLastError()ERROR_IO_PENDINGを返す操作はありませんReadFile。つまり、バックグラウンドでの作業に時間がかからないことを意味します。 これが発生するのはなぜですか?

非同期操作用にコーディングした場合でも、I/O 操作が同期的に完了する理由はいくつかあります。

圧縮

非同期操作の障害の 1 つは、新しいテクノロジ ファイル システム (NTFS) 圧縮です。 ファイル システム ドライバーは、圧縮ファイルに非同期的にアクセスしません。代わりに、すべての操作が同期されます。 この障害は、COMPRESS や PKZIP のようなユーティリティで圧縮されたファイルには適用されません。

NTFS 暗号化

圧縮と同様に、ファイル暗号化により、システム ドライバーは非同期 I/O を同期に変換します。 ファイルが暗号化解除された場合、I/O 要求は非同期になります。

ファイルを拡張する

I/O 操作が同期的に完了するもう 1 つの理由は、操作自体です。 Windows では、長さを拡張するファイルへの書き込み操作は同期操作になります。

注:

アプリケーションでは、 関数を使用 SetFileValidData してファイルの有効なデータ長を変更し、 を発行することで、前述の書き込み操作を WriteFile非同期にすることができます。

(Windows XP 以降のバージョンで使用できる) を使用すると SetFileValidData 、アプリケーションはファイルを効率的に拡張でき、ゼロフィルのパフォーマンス低下が発生しません。

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 要求が非同期であることを保証する最善の方法です。 これは、キャッシュ メカニズムをまったく使用しないようにファイル システムに指示します。

注:

このフラグの使用には、データ バッファーの配置とデバイスのセクター サイズに関連するいくつかの制限があります。 詳細については、このフラグの適切な使用に関する 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 を指定しない既定のファイル アクセスは、同期およびキャッシュされた操作です。

注:

ファイル システム ドライバーは、変更されたデータの予測非同期読み取り先と非同期遅延書き込みを行うため、このモードでは自動非同期動作が発生します。 この動作によってアプリケーションの I/O が非同期になるわけではありませんが、大多数の単純なアプリケーションにとって理想的なケースです。

一方、アプリケーションが単純でない場合は、この記事で前に示したテストと同様に、最適な方法を判断するために、プロファイリングとパフォーマンスの監視が必要になる場合があります。 または WriteFile 関数に費やされた時間をReadFileプロファイリングし、この時間を実際の I/O 操作が完了するまでにかかる時間と比較すると便利です。 ほとんどの時間が実際に I/O を発行する際に費やされた場合、I/O は同期的に完了します。 ただし、I/O 要求の発行にかかった時間が、I/O 操作の完了にかかる時間と比較して比較的少ない場合、操作は非同期的に処理されます。 この記事で前述したサンプル コードでは、 関数を QueryPerformanceCounter 使用して独自の内部プロファイルを実行します。

パフォーマンス監視は、プログラムがディスクとキャッシュを使用している効率を判断するのに役立ちます。 Cache オブジェクトのパフォーマンス カウンターを追跡すると、キャッシュ マネージャーのパフォーマンスが示されます。 物理ディスクまたは論理ディスク オブジェクトのパフォーマンス カウンターを追跡すると、ディスク システムのパフォーマンスが示されます。

パフォーマンス監視に役立つユーティリティがいくつかあります。 PerfMonDiskPerf は特に便利です。 システムがディスク・システムのパフォーマンスに関するデータを収集するには、まずコマンドを発行する DiskPerf 必要があります。 コマンドを発行した後、システムを再起動してデータ収集を開始する必要があります。

関連情報

同期および非同期 I/O