非同期 I/O (3/4) 完了ルーチン

4. 完了ルーチン

4.1 概要

前述の方法では、 GetOverlappedResult を呼び出して、I/O の完了をチェックしました。ここで説明する方法では、APC (Asynchronous Procedure Call, 非同期プロシージャコール) を使います。

それぞれのスレッドは、APC キューを持ちます。スレッドが 「警告可能な待機状態 (alertable wait state)」 になったときに、システムは APC キューをチェックし APC キューにエントリがある場合にはその APC を処理します。逆に言うとアラータブルウェイトにならないと、APC にエントリがあっても実行されません。スレッドは、SleepEx や WaitForSingleObjectEx などをアラータブルオプション付きで呼び出したときに、アラータブルウェイト状態に入ります。

制限としては、APC キューにエントリを追加するスレッドと、APC キューをチェックしてそれを実行するスレッドが同一でなければならない、とい うことです。これは結構厄介な制限です。たいていの場合、次節で詳解する IOCP を利用することになるでしょうし、そちらの方が優れています。

APC を利用するのは ReadFileEx です。

BOOL ReadFileEx(
  HANDLE hFile,
  LPVOID lpBuffer,
  DWORD nNumberOfBytesToRead,
  LPOVERLAPPED lpOverlapped,
  LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

I/O が完了したときに、APC がポストされます。その状態で、ReadFileEx を呼び出したスレッドが警告可能な待機状態に入ったところで、APC が取り出されて lpCompletionRoutine で指定した完了ルーチン (Completion Routine) が呼び出されます

4.2 コードの解説

では、早速サンプルコードの解説を行います。こちらからダウンロードしてください。

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

この中でポイントを抜粋して、コメントします。

ヘッダーファイルは前回とほぼ同様です。関数名は比較しやすいように、前回と同様の名前をつけました。

UI からパス名を取得した後、BeginAsyncRead 関数から処理をスタートさせています。

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

    // ファイルを開く
    // やはり FILE_FLAG_OVERLAPPED フラグを指定します。 
    HANDLE hFile = CreateFile ( 
        pszPathFrom, 
        GENERIC_READ,
        NULL,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_OVERLAPPED,
        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;
 
    // このサンプルではここでいきなりスレッドを生成します。
    // 前述のように、ReadFileEx を呼び出すスレッドにて APC のチェックを入れますが、
    // このスレッドは UI スレッド (メインスレッド) ですから、SleepEx を呼び出すのに
    // 都合が悪いからです。コンテキスト情報は、スレッド関数への引数として渡します。
    // こんなシンプルな例だとわざわざスレッドを立てて非同期で ReadFileEx を呼び出すのは、
    // 単に面倒なだけですけど...
    pContext->hThread = (HANDLE) _beginthreadex(
        NULL, 
        NULL, 
        WorkerThreadFunc
        (void*) pContext
        NULL, 
        &pContext->uThreadId);

    ....
}

_beginthreadex で起動されるスレッド関数は次です。ここで ReadFileEx API を呼び出しています。

unsigned int __stdcall WorkerThreadFunc (PVOID pv) {

    DebugPrint ( TEXT("WorkerThreadFunc %08x Start\n"), GetCurrentThreadId() );
    
    PREAD_CONTEXT pContext = (PREAD_CONTEXT) pv;

    // 第5引数が完了ルーチンの名前指定です。ここではその名の通り、
    // FileIOCompletionRoutine という名前を使っていますが、名前はユーザー定義です。
    // この読み込みが完了し、かつ、アラータブルなウェイト状態に入ると FileIOCompletionRoutine 
    // が呼び出されます。

    BOOL bRet = ReadFileEx (
        pContext->hFile,
        pContext->pBuffer,
        BUFFER_SIZE,
        &pContext->ol,
        FileIOCompletionRoutine );

    ....
    // ReadFileEx を呼び出した後、SleepEx を呼び出すループに入ります。
    // 二つ目の引数が TRUE ですから、これがすなわち Alertable な waite state に
    // 入っているところです。
    while ( SleepEx ( INFINITE, TRUE ) ) {
    } 

    // ちなみに、この部分は通りません。FileIOCompletionRoutine にて ExitThread で
    // このワーカースレッドを終了させるようにしているからです。
    DebugPrint ( TEXT("WorkerThreadFunc %08x End\n"), GetCurrentThreadId() );

    return 0;
}

I/O が完了したときに呼び出される FileIOCompletionRoutine は次の通りです。特徴的なのは、引数として OVERLAPPED 構造体へのポインタ (OVERLAPPED 関数のアドレス) が渡されるということです。

VOID CALLBACK FileIOCompletionRoutine
    DWORD dwErrorCode, 
    DWORD dwNumberOfBytesTransferred
    LPOVERLAPPED lpOverlapped ) {

    // 完了ルーチンには、データの入っているバッファなどが渡されるのではなく、
    // OVERLAPPED 構造体へのポインタが渡されます。ですから、このデータから
    // READ_CONTEXT 構造体の先頭ポインタを取得するには、
    // CONTAINING_RECORD マクロを使います。このマクロは winnt.h で定義されています。
   

    PREAD_CONTEXT pContext = (PREAD_CONTEXT) CONTAINING_RECORD (lpOverlapped, READ_CONTEXT, ol);

    // 第二パラメータには転送されたデータ量が入ります。

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

    // READ_CONTEXT の pBuffer に転送されたデータが格納されているのでそれを使う。
    .... WriteFile (...

    // もっとデータを読み込まなければいけないのかどうか判定。
    // このサンプルではあらかじめファイルサイズを取得して、それとこれまでに読み取った
    // サイズを比較しています。
    if ( pContext->cbFileSize == pContext->cbTotalBytesRead ) {
        ...  
        // このスレッドを終了します。このスレッドは SleepEx のスレッドでしたから
        // 無限ループに入っていた(ように見えた) WorkerThreadFunc も無事
        // 終了します。
        ExitThread (0);
        return;
    }
  
    // ファイルの最後まで読み込むために、また ReadFileEx を呼び出します。
    // こうして大きなバッファを確保することを防いでいます。
    BOOL bRet = ReadFileEx (
        pContext->hFile,
        pContext->pBuffer,
        min (BUFFER_SIZE, (pContext->cbFileSize - pContext->cbTotalBytesRead)),
        &pContext->ol,
        FileIOCompletionRoutine );

    ...
 
}

 4.3 確認

念のため、アラータブルウェイト状態に入る部分と APC を処理する箇所 (コールバック関数が呼び出される箇所) が確かに同じスレッドで動作していることを確認しておきます

方法はいろいろありますが (例えばスレッドID をデバッグトレースに出力)、ここではデバッガで直接動作を見てみます。

FileIOCompletionRoutine にブレークポイントを設定して、ブレークポイントがヒットしたところでスタックバックトレースを見ます。


Breakpoint 0 hit
eax=00000000 ebx=003c2520 ecx=00019000 edx=00000000 esi=03369060 edi=0354fb44
eip=004013f0 esp=0354fb1c ebp=0354fb30 iopl=0         nv up ei pl zr na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
dlgwin32!FileIOCompletionRoutine:
004013f0 55               push    ebp
0:001> kbn
 # ChildEBP RetAddr  Args to Child
00 0354fb18 7c838575 00000000 00019000 03369060 dlgwin32!FileIOCompletionRoutine [...\dlgwin32.cpp @ 196]
01 0354fb30 7c90eac7 004013f0 03369060 00000000 kernel32!BasepIoCompletionSimple+0x2e
02 0354fe68 0040162f ffffffff 00000001 00000000 ntdll!KiUserApcDispatcher+0x7
03 0354ff80 004019e0 03350048 7c97e4c0 7c9140ef dlgwin32!WorkerThreadFunc+0xaf [...\dlgwin32.cpp @ 309]
04 0354ffb4 7c80b50b 003c2520 7c97e4c0 7c9140ef dlgwin32!_threadstartex+0x6f [...\threadex.c @ 241]
05 0354ffec 00000000 00401971 003c2520 00000000 kernel32!BaseThreadStart+0x37

この状況は、スレッドが開始して、WorkerThreadFunc に入り、その 309 行目から ntdll!KiUserApcDispather、kernel32!BasepIoCompletionSimple を経て dlgwin32!FileIOCompletionRoutine が呼び出されたことを示しています。KiUserApcDispatcher といういかにも APC の処理を適切なルーチンにディスパッチしてくれるような (アンドキュメント) 関数が見えます。

さて、そこでソースと比較すると、確かに dlgwin32.cpp の 309 行目は次のラインでした (実験当時)。

309:    while ( SleepEx ( INFINITE, TRUE ) ) {

簡単ですが、これで SleepEx から入り、FileIOCompletionRoutine が呼び出されたことが確認できました。

 

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

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