非同期 I/O (2/4) OVERLAPPED

3. 基本的な非同期 I/O

3.1 概要

非同期 I/O を開始するための典型的な手順は次のようになります。

  1. ファイルを開くときに OVERLAPPED を指定する。
    CreateFile(Ex) ならば FILE_FLAG_OVERLAPPED を指定。ソケットの場合は WSASocket にて WSA_FLAG_OVERLAPPED フラグ等。
  2. 読み込み・書き込み操作の時に OVERLAPPED 構造体を渡す。 

OVERLAPPED 構造体というのは次のように定義されています。

typedef struct _OVERLAPPED { 
    ULONG_PTR Internal; 
    ULONG_PTR InternalHigh; 
    DWORD Offset; 
    DWORD OffsetHigh; 
    HANDLE hEvent;
} OVERLAPPED

それぞれのフィールドは、ファイルの種類や呼び出し方によって、使ったり、使わなかったりいろいろです。

OVERLAPPED を ReadFile などの API に渡すときの注意としては、I/O の完了を受けたときにも OVERLAPPED がスコープから外れていないように気をつける、 ということです。

このフィールドの中の hEvent にイベントオブジェクトをセットすると、I/O が完了したときにシグナル状態にセットされます。必要に応じて使います。

以上を踏まえて、サンプルコードを説明します。

サンプルコードのダウンロード [basic.zip, makefileを使用]

3.2 コードの説明

まずはヘッダーファイルからです。

#pragma once

#include <windows.h>
#include <windowsx.h>

#define BUFFER_SIZE  (100 * 1024)   // 100kb

// 非同期読み込みに使うバッファ、読み込みたいデータサイズ、読み込んだデータサイズ及び
// OVERLAPPED 構造体など、非同期読み込みで使うデータをこのような形で一まとめにしておきます。

typedef struct _READ_CONTEXT {

    HANDLE  hFile;
    DWORD  cbTotalBytesRead;
    DWORD  cbFileSize;
    BYTE  pBuffer [BUFFER_SIZE];  // 読み込みバッファ
    HANDLE  hSaveFile;
    // ワーカースレッドの情報
   
HANDLE  hThread;
    UINT  uThreadId;
    // OVERLAPPED 構造体
    OVERLAPPED ol;

} READ_CONTEXT, *PREAD_CONTEXT;


///////////////////////////////////////////////////////////////////////////////
// こちらは単にダイアログで使うメッセージクラッカで す。

#define HANDLE_DLG_MSG(hwnd, msg, fn) \
    case(msg): return SetDlgMsgResult(hwnd, msg, HANDLE_##msg(hwnd, wParam, lParam, fn))

///////////////////////////////////////////////////////////////////////////////

BOOL CALLBACK DialogProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
BOOL OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam);
void OnCommand(HWND hWnd, int nID, HWND hWndCtl, UINT codeNotify);
unsigned int __stdcall WorkerThreadFunc(PVOID pv);
VOID CleanupContext ( PREAD_CONTEXT pContext );
VOID BeginAsyncRead ( HWND hwnd, LPTSTR pszPathFrom, LPTSTR pszPathTo );

ここで注意するのは、OVERLAPPED 構造体をメンバーフィールドとして含む、READ_CONTEXT とうデータ構造を定義しているところです。名前は READ_CONTEXT でなくても何でもいいのですが、ポイントは OVERLAPPED を含む、というところです。ワンセットにして保持しておくと、何かと都合が良いです (特に状況が複雑になる IOCP の場合で)。

次は CPP ファイルです。長いので要点をかいつまんで説明します。ポイントは BeginAsyncRead からの呼び出しです。この関数はコピー元のファイルとコピー先のファイル名を受け取ります。

VOID BeginAsyncRead ( HWND hwnd, LPTSTR pszPathFrom, LPTSTR pszPathTo ) {

    //
    // ファイルを開く
    //

    HANDLE hFile = CreateFile ( 
        pszPathFrom, 
        GENERIC_READ,
        NULL,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_OVERLAPPED// 非同期 I/O の指定
        NULL);

    ...

    //
    // コンテキストの作成
    //
 
    PREAD_CONTEXT pContext = (PREAD_CONTEXT) malloc ( sizeof (READ_CONTEXT) );
    DWORD cbFileSize = GetFileSize ( hFile, NULL ); 
    ... 
    ZeroMemory ( pContext , sizeof (READ_CONTEXT) );

    pContext->hFile = hFile;
    pContext->cbTotalBytesRead = 0;
    pContext->cbFileSize = cbFileSize;
    pContext->ol.Offset = 0;
    pContext->hSaveFile = hSaveFile;
 
    //
    // Read file
    //

    BOOL bRet = ReadFile (
        hFile,
        pContext->pBuffer,
        BUFFER_SIZE,
        NULL,
        &pContext->ol ); // OVERLAPPED を渡す

    DWORD dwGle = GetLastError ();

    // ReadFile の呼び出しが成功した場合、データが直ちに読み込めたとき、つまり非同期を指定しなくても
   // 呼び出しがブロックしない状況では、TRUE が返ります。FALSE が返された場合は、エラーコードを
   // チェックしてそれが ERROR_IO_PENDING であれば非同期読み込みが正常に開始できたことを示しています。

    if ( bRet || (!bRet && ERROR_IO_PENDING == dwGle) ) {

        ....
        // ここでデータを読み込む処理を開始します。本来ならばせっかく非同期にしているので、
        // スレッドをわざわざ起動しなくても良いのです。が、このサンプルはダイアログのボタンクリックの
       // メッセージハンドラからの処理ですから、UI をブロックさせるのはいやなので、新しくスレッドを
       // 起動して、そのスレッドで読み込みを行っています。

        pContext->hThread = (HANDLE) _beginthreadex(
            NULL, 
            NULL, 
            WorkerThreadFunc, 
            (void*) pContext, 
            NULL, 
            &pContext->uThreadId);

        ....

        // OK
        return;
    }
    else {
        ...
    }

cleanup:
    // Error
    CleanupContext ( pContext );
}

...


unsigned int __stdcall WorkerThreadFunc (PVOID pv) {

    DebugPrint ( TEXT("WorkerThreadFunc %08x Start\n"), GetCurrentThreadId() );
    PREAD_CONTEXT pContext = (PREAD_CONTEXT) pv;
    LPOVERLAPPED pol = NULL;
    DWORD cbNumberOfBytesTransferred = 0;
    BOOL bRet;

    while (1) {

        // GetOverlappedResult の第一引数で指定したファイルの読み込み要求が
        // 完了したところで GetOverlappedResult が制御を返します。ここで、第三引数に
        // 転送されたデータのバイト数が格納されます。
        //
        // ここで第四引数に TRUE を指定していますから、I/O が完了するまでブロックします。
        // FALSE を指定すると、GetOverlappedResult は直ちに制御を返します。このときの戻り値は
        // FALSE になり、GetLastError は ERROR_IO_INCOMPLETE がセットされます。

        bRet = GetOverlappedResult ( pContext->hFile, &pContext->ol, &cbNumberOfBytesTransferred, TRUE );

        ...

        DebugPrint ( TEXT("* GetOverlappedResult returned\nTransferred: %u\n"), cbNumberOfBytesTransferred );

        //
        // コンテキストの情報を更新
        // 
        // このコンテキストデータは、受け取った総バイト数や OVERLAPPED 構造体を保持しています。
        // cbNumberOfBytesTransferred を元に総バイト数と、次のデータ読み込み要求に使用する
        // ためのオフセット値をセットしています。

        pContext->cbTotalBytesRead += cbNumberOfBytesTransferred;
        pContext->ol.Offset += cbNumberOfBytesTransferred;

        //
        // データの保存
        //
        // ここでは受け取ったデータを利用する応用のひとつの例として、
        // データをファイルに保存しています。

        DWORD cbNumberOfBytesWritten = 0;
        BOOL bWrite;
        bWrite = WriteFile ( 
            pContext->hSaveFile, 
            pContext->pBuffer,
            cbNumberOfBytesTransferred,
            &cbNumberOfBytesWritten,
            NULL );

        ....

        //
        // データ読み込み終了判定としてデータ読み込み総数をカウントし、
        // それがあらかじめ取得していたファイルサイズに等しくなったところで
        // 終了としています。ファイル読み込みなど EOF が使える場合は
        // 他にやりようがあるかもしれません。
        //

        if ( pContext->cbFileSize == pContext->cbTotalBytesRead ) {
             ...
            // ワーカースレッドを終了します
            break;
        }

        //
        // 必要に応じさらにデータを読み込みます
        //

        bRet = ReadFile (
            pContext->hFile,
            pContext->pBuffer,
            min (BUFFER_SIZE, (pContext->cbFileSize - pContext->cbTotalBytesRead)),
            NULL,
            &pContext->ol );

        ....

        // GetOverlappedResult を呼び出しでブロックする前に
        // 他の処理をする場合はここで行う。
        //(GetOverlappedResult をブロックさせずに ERROR_IO_INCOMPLETE を利用する場合は、
        //  そちらの分岐で行います。どちらを使うかは状況によります。)

    }

    DebugPrint ( TEXT("WorkerThreadFunc %08x End\n"), GetCurrentThreadId() );

    return 0;
}

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説