WaitableTimerで時間を測る

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

Windowsアプリで、タイマーで時間を測るときに、Windowsがスリープに入っている間も時間をカウントしたいと思った。

C#で、Thread.Sleep(10 * 60 * 1000)で10分待っている間に、Windowsを1分スリープさせて戻ってくると、実時間で11分経たないと、アプリのThread.Sleepが満了しないので、その1分遅れをせずに、10分で満了させたい)

WaitableTimerというのを使うとそういうことができるという話を聞いたので、試してみる。

試した結果

結論、できそうではある。

が、副作用がありそうな感じがするので、そこを把握して使わないといけなさそう。

→その副作用をしっかり把握するには、WindowsPCのハード的な部分がどうなってるのか、や、Windowsの設定等がどうなってるのか(スリープからWaitableTimerでレジュームできる構成なのか、設定なのか)を把握してないといけなさそうなのだが、それが手元にある自宅の古いPCだけでは実験/検証できなさそう&知識不足なので、今回はこれを使うのはやめようと思う。

ただせっかく実験したので、その時にやったメモは残そうと思う。

実験コード

https://github.com/tera1707/CppTimerTest

やったこと

下記のようなコードを書いてWaitableTimerを実験した。

基本の使い方は、

  • CreateWaitableTimerして
  • SetWaitableTimerをよぶ

下記のような感じ。

#include <windows.h>
#include <stdio.h>

HANDLE hTimer = NULL;
LARGE_INTEGER liDueTime;
long long sec = 5LL;//タイマ満了時間(秒)

/// <param name="lpArg">SetWaitableTimerの第四引数で渡すパラメータ</param>
/// <param name="dwTimerLowValue">時刻が載ってるらしい(使う気なし)</param>
/// <param name="dwTimerHighValue">時刻が載ってるらしい(使う気なし)</param>
VOID CALLBACK TimerAPCProc(LPVOID lpArg, DWORD dwTimerLowValue, DWORD dwTimerHighValue)
{
    auto param = (int*)lpArg;
    printf("タイマ満了来ました。%d\r\n", *param);
}

// 満了時コールバック無し、シグナル待ちするパターン
// fResume:SetWaitableTimerのタイマ満了時に、スリープをresumeさせるかどうか
// →これをTRUEにしていると、スリープ/休止中もカウントを継続できる。で、スリープ中に満了したら、ハードやwindowsの設定が許せば、resumeするらしい。(手持ちPCでは起動しなかった))
void SetWaitableTimerWithoutCallback(HANDLE hTimer, LARGE_INTEGER liDueTime, BOOL fResume)
{
    MessageBox(NULL, L"start", L"", MB_OK);
    if (!SetWaitableTimer(hTimer, &liDueTime, 0, NULL, NULL, fResume))
    {
        printf("SetWaitableTimer 失敗 (%d)\r\n", GetLastError());
        return;
    }
    printf("SetWaitableTimer 成功 (%d)\r\n", GetLastError());

    // タイマー満了待ち
    if (WaitForSingleObject(hTimer, INFINITE) != WAIT_OBJECT_0)
    {
        printf("WaitForSingleObject 失敗 (%d)\r\n", GetLastError());
    }
    else
    {
        printf("タイマーがシグナル状態になりました\r\n");
    }
    MessageBox(NULL, L"stop", L"", MB_OK);
}

void SetWaitableTimerWithCallback(HANDLE hTimer, LARGE_INTEGER liDueTime, BOOL fResume)
{
    MessageBox(NULL, L"start", L"", MB_OK);

    // 満了時コールバック有りパターン
    int param = 99;
    if (!SetWaitableTimer(hTimer, &liDueTime, 0, TimerAPCProc, &param, fResume))
    {
        printf("SetWaitableTimer 失敗 (%d)\r\n", GetLastError());
        return;
    }
    // 注意!Sleep()で待っていると、ここと同じスレッドで実行されることになるTimerAPCProcが呼べなくなる。
    // WaitableTimerを使っている場合にスレッドをスリープさせたい場合は、SleepEx()で行う必要がある。
    // (SleepEx()の第三引数はTRUE=アラート可能である必要がある。)
    // また、タイマ満了した結果、SleepEx()の待ちは終了する
    SleepEx(INFINITE, TRUE);

    MessageBox(NULL, L"stop", L"", MB_OK);
}

int main()
{
    auto oneSec = -10000000LL;// 1=100nsec(ナノ秒)なので、これで1秒になる
    liDueTime.QuadPart = sec * oneSec;

    // タイマ作成
    hTimer = CreateWaitableTimer(NULL, TRUE, L"aaa");
    if (NULL == hTimer)
    {
        printf("CreateWaitableTimer 失敗 (%d)\r\n", GetLastError());
        return 1;
    }
#if 0
    SetWaitableTimerWithoutCallback(hTimer, liDueTime, FALSE);
#else
    SetWaitableTimerWithCallback(hTimer, liDueTime, FALSE);
#endif

    return 0;
}

SetWaitableTimerのパラメータ「fResume」は、SetWaitableTimerのタイマ満了時に、スリープをresumeさせるかどうかのフラグ。 これをTRUEにしていると、スリープ/休止中もカウントを継続できる。(今回やりたかったことはできる。)

で、スリープ中に満了したら、ハードやwindowsの設定が許せば、resumeするらしい。(手持ちPCでは起動しなかった))
(FALSEだと、スリープに入るとその間カウントが止まって、スリープ明け(resume後)に、カウント再開するので、スリープしてた分、満了までの時間が延びる)

問題

コールバックありのときに、実験コードのコンソールが終わってしまわないように、Sleep();で待たせていたのだが、それをすると、満了するはずの時間が経過しても、コールバックが呼ばれてくれない。

公式によると、Sleep()を使うとそうなってしまうので、SleepEx()を使うべしとのこと。

そのあたりの関連かどうかわからないが、以前作ったダイアログベースのアプリの中で、今回の実験コードを持って行ってタイマをかけても、タイマ満了のコールバックを呼んでくれなかった。

(SetWaitableTimerしたあとに、普通にダイアログProcに戻っていった場合。ダイアログProcに戻る前に、ボタンを押したコマンドの中で、SleepEx(INFINITE, TRUE);をすると、満了コールバックに来てくれる。

→ダイアログProcの中で、コールバックを呼べない待ち方をしている??(想像)

TimerQueueTimerについて

SetWaitableTimerには、試した限り、上記のような問題があったのだが、以前別途試したTimerQueueTimerだと、ダイアログベースでもうまく動作させることができた。

https://qiita.com/tera1707/items/9d07f179175068ceaa89

スリープ中にもタイマーカウント継続して、満了したらスリープ解除したい、とかの特殊な要件がなければ、個人的にはTimerQueueTimerのほうを使った方がわかりやすそう&変なところでバグらなさそうに感じた。

備考

もしかしたら、Windowsの下記設定で、WaitableTimerでスリープからresumeするかどうか、設定できるのかも?(すみません、未確認)

参考

待機可能タイマー オブジェクトの使用

https://learn.microsoft.com/ja-jp/windows/win32/sync/using-waitable-timer-objects

非同期プロシージャ 呼び出しでの待機可能タイマーの使用

https://learn.microsoft.com/ja-jp/windows/win32/sync/using-a-waitable-timer-with-an-asynchronous-procedure-call

SetWaitableTimer 関数 (synchapi.h)

https://learn.microsoft.com/ja-jp/windows/win32/api/synchapi/nf-synchapi-setwaitabletimer

CreateWaitableTimerW 関数 (synchapi.h)

https://learn.microsoft.com/ja-jp/windows/win32/api/synchapi/nf-synchapi-createwaitabletimerw

Wake the PC from standby or hibernation

https://www.codeproject.com/articles/49798/wake-the-pc-from-standby-or-hibernation?fid=1556013&df=90&mpp=25&prof=True&sort=Position&view=Normal&spc=Relaxed&fr=26

実験コード

https://github.com/tera1707/CppTimerTest