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

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

いくつかの関数は、同期および非同期の I/O に関連しています。 この記事では、例として使用 ReadFile します WriteFile 。 適切な代替手段は次のようになります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 操作の完了後に転送された実際のバイト数を決定するために使用されます。 渡 &NumberOfBytesRead された内容 ReadFile は意味がありません。

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

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

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

非同期 I/O は引き続き同期と見なされます

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

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

圧縮

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

NTFS 暗号化

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

ファイルを拡張する

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

注:

アプリケーションは、関数を使用SetFileValidDataしてファイルの有効なデータ長を変更し、.WriteFile

(Windows XP 以降のバージョンで利用できる) を使用すると SetFileValidData 、アプリケーションは、ゼロフィルのパフォーマンスに影響を与えることなく、効率的にファイルを拡張できます。

NTFS ファイル システムでは、定義されている有効なデータ長 (VDL) までデータがゼロフィルされないため、この関数は、以前に他のファイルで SetFileValidData占有されていたクラスターをファイルに割り当てる可能性があるセキュリティ上の影響があります。 そのため、 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、この種類のアクセスに対してキャッシュを最適化します。 ただし、ランダムな方法でファイルにアクセスする場合は、フラグCreateFileFILE_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