LoginSignup
1
1

[C++] TimerQueueTimerで時間を測る

Last updated at Posted at 2021-06-07

もくじ

やりたいこと

C++でできたWindowsサービスのプログラムで、一定時間たった後に〇〇の処理をしたい、というようなことがあった。

で、C++でダイアログアプリを作っていた時のように、SetTimer()を使おうと思ったが、なんか自分でメッセージループを作らないといけない?とかでパッと使えなさそうだったので、何か別の方法ないかと探したところ、タイマーキュータイマーというのがあった。

それを使ってみる。

基本の流れ

タイマーをかけるときには、

  1. タイマーQueをつくる
  2. タイマーQueにタイマーをつくる
  3. タイマーQueに作ったタイマーを止める
  4. タイマーQueを削除する

という流れでやるらしい。
それぞれ下記の関数を使う。

タイマーQueをつくる

タイマーQueにタイマーをつくる

タイマーQueに作ったタイマーを止める

タイマーQueを削除する

サンプルコード

#include <iostream>
#include <windows.h>
#include <stdio.h>
/*
・プログラムの最初でタイマーキューを作成(CreateTimerQueue())
・タイマーをstartさせるところでCreateTimerQueueTimer()
・CreateTimerQieieTimerしたタイマは必ずDeleteTimerQueueTimer()必要
・但し満了ハンドラの中でDeleteTimerQueueTimerはできない(エラーになる)ので、
 CreateTimerQueueTimer()する前にhTimerがNULLでなければDeleteTimerQueueTimer()し、
 DeleteTimerQueueTimer()後には必ずhTimer=NULLにする、とかにしとけばOKと思われる
*/

//////////////////////////////////////////////////////////////////
// かかってたら止める→タイマスタート→満了、をひたすら繰り返して
// メモリリークしないか確認するプログラム
//////////////////////////////////////////////////////////////////

HANDLE hTimerQueue = NULL;
HANDLE hTimer = NULL;

VOID CALLBACK TimerRoutine(PVOID lpParam, BOOLEAN TimerOrWaitFired)
{
    printf("TimerRoutine 来ました\n");
}

int main()
{
    int ctr = 0;

    // プログラム開始時に一回タイマーキューを作成
    hTimerQueue = CreateTimerQueue();
    if (NULL == hTimerQueue)
    {
        printf("CreateTimerQueue failed (%d)\n", GetLastError());
        return -1;
    }

    for (int i = 0; i < 10000; i++)
    {
        printf("%d回目\n", ctr);

        // すでにタイマがかかっていたら一度タイマ削除する(満了後だったとしてもDeleteしないとメモリリークする)
        if (hTimerQueue != NULL && hTimer != NULL)
        {
            if (!DeleteTimerQueueTimer(hTimerQueue, hTimer, NULL))
            {
                printf("DeleteTimerQueueTimer failed (%d)\n", GetLastError());
                return -3;
            }
            else
            {
                // 削除成功
                hTimer = NULL;
                printf("delOK!\n");
            }
        }

        // タイマースタート
        if (!CreateTimerQueueTimer(&hTimer, hTimerQueue,
            (WAITORTIMERCALLBACK)TimerRoutine, NULL, 1, 0, 0))
        {
            printf("CreateTimerQueueTimer failed (%d)\n", GetLastError());
            return -2;
        }

        Sleep(50);
        
        ctr++;
    }


    // プログラムの終わりで、タイマーキューを削除する
    if (!DeleteTimerQueueEx(hTimerQueue, NULL))
        printf("DeleteTimerQueue failed (%d)\n", GetLastError());

    printf("Timer Queue を Deleteしました...\n");

	system("pause");
}

注意点

基本の流れ補足

取り合えずなにかのトリガーをきっかけにして、そこから〇秒を測りたい、というときは、

  • タイマーQueをつくるCreateTimerQueue()は最初に一回だけ呼ぶ。
  • タイマーをstartしたいときにCreateTimerQueueTimer()を呼び、
  • タイマーをstopしたいときにDeleteTimerQueueTimer()を呼ぶ。
  • タイマーQueを削除するDeleteTimerQueue()は、最後に一回だけ呼ぶ。

という流れにする。

1回だけ時間を測りたいとき

CreateTimerQueueTimer()でタイマをかけるときに、第六引数のPeriodを0にする。
image.png
そうすると、1回だけ時間を測って、CallBackにセットした関数が、第五引数のDueTimeにセットした時間(単位:ms)が経過したときに1回だけ呼ばれてくれる。

逆に、Periodにセットした時間経過時に何度もCallbackが呼ばれてほしいときは、Periodを0以外の値にする。そうすると、CreateTimerQueueTimer()を呼んでタイマスタート後、DueTime経過時に一度CallBackが呼ばれ、その後Period経過したときに繰り返しCallBackを呼んでくれる。

メモリリーク

上の「1回だけ時間を測りたいとき」のように、Periodを0にしてCreateTimerQueueTimer()を呼ぶとDeleteTimerQueueTimer()を呼ばなくてもタイマ動作自体は止まってくれるが、CreateTimerQueueTimer()で確保したハンドルを使ってDeleteTimerQueueTimer()を呼んでタイマのハンドルを開放しないと、メモリリークしてしまう。

上のサンプルコードで、DeleteTimerQueueTimer()の部分を削除すると、タスクマネージャで見たときに、少しずつメモリの値が増えていってしまう。

※繰り返しタイマーQueを作成、Que自体を削除、ということをする場合も、Queの削除を忘れるとメモリリークする。

DeleteTimerQueueTimer()をするタイミング

いまだにちょっとわかってないのが、**どこでDeleteTimerQueueTimer()をするのが正しいのか?**ということ。

例えば、上のコードで

VOID CALLBACK TimerRoutine(PVOID lpParam, BOOLEAN TimerOrWaitFired)
{
    if (!DeleteTimerQueueTimer(hTimerQueue, hTimer, NULL))
    {
        printf("DeleteTimerQueueTimer failed (%d)\n", GetLastError());
        return;
    }
}
// ※メイン関数のほうのDeleteTimerQueueTimer()は消しとく

という感じで、タイマの満了時実行してくれるハンドラの中でDeleteTimerQueueTimer()を実行しても、エラーになって正しくタイマを止めることができない。

image.png

エラーコード997=0x3E5は「ERROR_IO_PENDING」とのこと。

image.png

これをどうするのが正しいのかがわからない。

上のコードでは、次のタイマをかける前に前回のタイマ(hTimerハンドル)になにか入ってたらそいつをDeleteTimerQueueTimer()してやる、というようにしてクリアされるようにしているので、実質問題は起きないが、Periodを0にして1回だけ動くタイマの場合に、

  • 1回タイマ満了して止まったTimerQueueTimerが、次のタイマがCreateTimerQueueTimerされるまでは放置されてる

状態になるのが気持ち悪い。(その間は言ってしまえば「リーク」してる)

どうするのが正しいのか....

23/11/23追記 今時点の、正しいと思うDeleteTimerQueueTimer()をするタイミング

今は、

  • ある時点から一定時間が経過したことを知るためにタイマを使う(DueTimeに値を入れて使う)
  • 繰り返しタイマ発動はしなくていい(Period は 使わない、0でいい)
  • タイマ満了待ち中にもう一度同じタイマを書けたときは、最初のタイマはキャンセルさせて、後のタイマの満了だけ知れたらいい

という使い方をする前提であれば、上に書いた「CreateTimerQueueTimer()をする直前に、すでに動いているタイマがあればそれをDeleteTimerQueueTimer()する」でよいと思う。

理由は、、、

下記に、エラーコード997=0x3E5「ERROR_IO_PENDING」が帰ってくる理由が書いてあって、

image.png

未処理のコールバック関数があり、 CompletionEventがNULLの場合、関数は失敗し、  
エラー コードがERROR_IO_PENDINGに設定されます。  
これは、未処理のコールバック関数があることを示します。  
これらのコールバックは、実行されるか、実行中です。コールバック関数の実行が終了すると、  
タイマーはクリーンアップされます。

とのこと。

おそらく、タイマ満了ハンドラの中を実行している最中は、「まだ未処理のコールバック関数がある」状態なので、そのエラーになるのだと思われる。(まだやること残ってるのに途中でタイマ終わらせられないよということか)

上に書いたような気持ち悪さは、DeleteTimerQueue()をするのと一緒に(DeleteTimerQueue()をする直前に)DeleteTimerQueueTimer()をしてやれば、問題ないと思う。

そのようにしてやるのが最善だと今のところ思っている。

参考

MSのサンプルコード。
ここでは今回上でやったのとは少し異なる、イベントフラグを見てタイマーが終わるまで待つ、ということをしてる。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1