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

# もくじ

https://tera1707.com/entry/2022/02/06/144447

# やりたいこと

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

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

それを使ってみる。

## 基本の流れ

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

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

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

タイマーQueをつくる

https://docs.microsoft.com/ja-jp/windows/win32/api/threadpoollegacyapiset/nf-threadpoollegacyapiset-createtimerqueue

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

https://docs.microsoft.com/ja-jp/windows/win32/api/threadpoollegacyapiset/nf-threadpoollegacyapiset-createtimerqueuetimer

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

https://docs.microsoft.com/ja-jp/windows/win32/api/threadpoollegacyapiset/nf-threadpoollegacyapiset-deletetimerqueue

タイマーQueを削除する

https://docs.microsoft.com/ja-jp/windows/win32/api/threadpoollegacyapiset/nf-threadpoollegacyapiset-deletetimerqueueex

# サンプルコード

```cpp
#include <iostream>
#include <windows.h>
#include <stdio.h>
/*
・プログラムの最初でタイマーキューを作成（CreateTimerQueue()）
・タイマーをstartさせるところでCreateTimerQueueTimer()
・CreateTimerQieieTimerしたタイマは必ずDeleteTimerQueueTimer()必要
・但し満了ハンドラの中でDeleteTimerQueueTimerはできない(エラーになる)ので、
　CreateTimerQueueTimer()する前にｈTimerが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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/c65e7fcd-42e8-251e-7d48-3cff9212a1b2.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()`をするのが正しいのか？**ということ。

例えば、上のコードで

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/01c88461-f50b-5459-fe1c-6b169cbff270.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/f452e400-e423-e6d0-c979-5d418aab7185.png)

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

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

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

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

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

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

今は、

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

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

理由は、、、

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

https://learn.microsoft.com/en-us/windows/win32/api/threadpoollegacyapiset/nf-threadpoollegacyapiset-deletetimerqueuetimer#remarks

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/81a938f1-22d3-ba50-d799-a7609e3ddd89.png)

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

とのこと。

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

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

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

# 参考

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

https://docs.microsoft.com/ja-jp/windows/win32/sync/using-timer-queues