APC と Waitable Timer
はじめに
この資料では、Windows が実装している 「非同期プロシージャ呼び出し(APC, Asynchronous Procedure Call)」 、 「待機可能タイマー (Waitable Timer)」 及び 「警告可能待機 (Alartable Timer)」 の考え方を例を挙げて簡単に説明します。
サンプル
始めにコードサンプルとその実行結果をご覧ください。注目するところは、青字の箇所です。一見、スリープするだけのように見える SleepEx 呼び出しで、文字が出力されています。
#define _WIN32_WINNT (0x0501)
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <process.h>
VOID CALLBACK TimerAPCRoutine
(
PVOID pvArgToCOmpletionRoutine,
DWORD dwTimerLowValue,
DWORD dwTimerHighValue) {
printf("[Hello,
APC]");
}
void Foo() {
HANDLE hTimer = CreateWaitableTimer
(NULL, TRUE, NULL); // ---(1)
LARGE_INTEGER li = {0};
SetWaitableTimer
(
// ---(2)
hTimer,
&li,
3000,
TimerAPCRoutine,
// ---(3)
NULL,
FALSE);
while(1)
{
printf ("Sleeping... ");
SleepEx(INFINITE, TRUE);
printf (" OK.\n");
}
CloseHandle(hTimer);
}
unsigned int __stdcall WorkerThreadFunc(PVOID
pv) {
Foo();
return
0;
}
void main()
{
DWORD dwThread;
HANDLE hHandle =
(HANDLE)_beginthreadex(NULL, NULL, WorkerThreadFunc,
NULL, NULL, (unsigned int*)&dwThread);
if(NULL
== hHandle) {
printf("_beginthreadex Failed.\n");
return;
}
WaitForSingleObject (hHandle, INFINITE);
}
実行結果:
>apctest.exe
Sleeping... [Hello, APC] OK.
Sleeping... [Hello, APC] OK.
Sleeping... [Hello, APC] OK.
Sleeping... ^C
Foo 関数は while(1) {... にて無限ループに入っていますが(そのためこのコードは Ctrl+C で終了させてください)、その中で printf を使って "Sleeping..." という文字を出力し、それから SleepEx を呼び出し、その次に再度 printf で " OK" という文字を出力します。実行結果を見てわかるように、SleepEx の呼び出しで、確かに "[Hello, APC]" という文字が出力されています。これはどういうことでしょうか。
解説
このコード全体の流れとしては、main 関数でスレッドを新しく起動して、main 関数は単に新しく生成したスレッドが終了するのを待ちます。スレッドは _beginthreadex で起動し、返されたハンドルを WaitForSingleObject で待機します。(C ランタイムを使う場合は CreateThread ではなく _beginthreadex を使ってください) 新しく起動したスレッドの開始点は、WorkerThreadFunc という名前の関数です。この関数は unsigined int を戻り値とし、呼び出し規則が __stdcall であり、PVOID 型の引数をひとつ取る関数であれば、名前は何でも構いません。
WorkerThreadFunc にて Foo を呼び出します。この資料ではこの Foo 関数が大事なところです。
Foo ではまず CreateWaitableTimer を使ってタイマーを生成しています (1)。 次に SetWaitableTimer を使って (1) で作成したタイマーをアクティベートします (2)。SetWaitableTimer の引数で注目して欲しい点は、タイマーのインターバルと APC ルーチンです。タイマーのインターバルは 3000 ms つまり 3 秒間隔です。APC ルーチンはここでは TimerAPCRoutine という名前の関数としています(3)。APC ルーチンもプロトタイプだけがきまっており、名前は自由に付けられます。
VOID CALLBACK TimerAPCProc(
LPVOID lpArgToCompletionRoutine,
DWORD dwTimerLowValue,
DWORD dwTimerHighValue
);
CALLBACK というのは、__stdcall として定義されていますので読み替えても結構です。
さて、タイマーのインターバル毎に何が起きるのでしょうか?上記のようにセットしたタイマーは、三秒毎に指定した関数の呼び出し(ここでは TimerAPCRoutine) を、自分のスレッド (SetWaitableTimer を呼び出したスレッド) の APC キューに追加します。 この関数呼び出しは非同期 (Asynchronous) でおきますが、これを非同期プロシージャ呼び出し (APC, Asynchronous Procedure Call) といいます。それぞれのスレッドは、APC キュー (Asynchronous Procedure Call Queue) というキューを持っており、このキューには呼び出したい関数 (APC) を登録しておくことが出来ます。APC キューに、f1, f2, f3, f4 という APC が追加されれば、(先に追加された) f4, f3, f2, f1 の順番で APC が処理されます。つまり、「ここで作成したタイマーは呼び出し元の APC キューに、APC を登録する」 ということです。
一方、もとのスレッドは SleepEx で待機していました。第一引数にスリープ時間を指定するだけの Sleep とは違い、 SleepEx は第二引数に警告可能待機 (Alertable wait) にするかどうか指定するフラグを受け取ることが出来ます。警告可能待機として SleepEx を呼び出した場合、APC キューに APC が追加されると APC キューのエントリを全て実行してから制御を返します。したがって、このコード例では SleepEx の呼び出し箇所で APC が処理され (つまり TimerAPCRoutine が実行され) てから SleepEx が返ります。ここで APC ルーチンは、単に "[Hello, APC]" という文字列をプリント関数なので、SleepEx でその文字列がプリントされたことになります。