hozugawa.net · A neutral informational page

timeSetEventでタイマーを使った定期処理

Windows APIで一定間隔の処理を実行する方法には、SetTimer、待機可能タイマー、スレッドのスリープ、マルチメディアタイマーなどがあります。timeSetEventは、比較的短い周期でコールバック関数を呼び出せるマルチメディアタイマーAPIです。音声処理や簡易的な監視、周期的なデータ更新などで利用されてきました。

この関数は便利な反面、コールバックが実行されるスレッド、タイマーの精度、停止時の競合を意識する必要があります。単に指定したミリ秒ごとに処理が完了する機能ではないため、仕様を確認してから実装することが重要です。

方法 主な用途 周期の精度 実装上の特徴
SetTimer GUIの定期処理 標準的 ウィンドウメッセージで通知される
timeSetEvent 短周期の通知 比較的高い コールバック関数を使う
待機可能タイマー スレッド処理 高い 待機と組み合わせやすい
Sleepループ 簡単な反復処理 状況依存 ドリフトや停止処理に注意
スレッドプールタイマー 非同期処理 高い 現代的な設計に適する

timeSetEventの基本的な仕組み

timeSetEventは、指定した期間が経過したときにコールバックを呼び出す関数です。第1引数に周期、第2引数に許容するタイマー分解能、第3引数にコールバック、第4引数にユーザーデータ、第5引数に動作フラグを渡します。

定期的に呼び出すには、TIME_PERIODICを指定します。一度だけ通知したい場合はTIME_ONESHOTを使います。戻り値はタイマー識別子であり、開始に失敗した場合は0になります。識別子は後でtimeKillEventへ渡し、タイマーを停止します。

このAPIの宣言や関連定数を確認するときは、古いコード例だけに頼らず、Windows APIの資料や使用するWindows SDKのヘッダー定義も照合すると安全です。SDKやコンパイラーの設定によって、コールバック型や型名の扱いが異なる場合があります。

パラメーターとコールバックの読み方

代表的な呼び出しは次のような形です。

MMRESULT id = timeSetEvent(
    100,
    1,
    TimerProc,
    reinterpret_cast<DWORD_PTR>(context),
    TIME_PERIODIC | TIME_CALLBACK_FUNCTION
);

第1引数の100は100ミリ秒周期を表します。第2引数は要求する分解能で、値を小さくすると高い分解能を要求できますが、システム全体の電力消費やスケジューリング負荷に影響します。実際の通知間隔が常に指定値と一致するわけではありません。

コールバックは通常、void CALLBACK TimerProc(UINT, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR)に近い形式で定義します。第3引数のユーザーデータには、状態を保持する構造体へのポインターを渡せます。ただし、その構造体がコールバック実行中も有効であることを保証しなければなりません。

コールバック内で行う処理

コールバックはGUIスレッド上で実行されるとは限らず、メッセージループの処理関数として扱うこともできません。画面部品を直接操作したり、長時間のファイルI/Oを実行したりすると、応答停止や再入が発生する可能性があります。

定期処理の内容が重い場合は、コールバックではイベントを記録してワーカースレッドへ渡す設計が適しています。共有データを変更する場合は、クリティカルセクション、ミューテックス、アトミック操作などを使い、同じ処理が重なって実行されない仕組みを用意します。

コールバックで避けたい処理

処理時間が周期を超えると、次回通知との重なりや遅延が起こります。コールバックでは最小限の通知だけを行い、実作業を別の実行単位へ移すと、タイマーの安定性を保ちやすくなります。

開始と停止を安全に管理する

タイマーを作成したら、戻り値を必ず保存します。timeKillEvent(timerId)を呼ぶことで定期通知を停止できますが、停止を要求した時点で、すでにコールバックが実行中である可能性があります。そのため、タイマー識別子を消すだけでは終了処理として不十分です。

アプリケーション終了時には、まず新しい通知を止め、コールバックの実行完了を待ち、その後にコンテキストや関連オブジェクトを解放します。コールバック側で参照カウントや終了フラグを使うと、破棄済みメモリーへのアクセスを防ぎやすくなります。

終了処理で確認する項目

タイマーをクラスで包む場合は、デストラクターで機械的に解放するだけでなく、コールバックからオブジェクトを参照する経路も管理します。停止処理と破棄処理を同じロックで保護すると、終了時の競合を整理しやすくなります。

精度とシステム負荷の考え方

timeSetEventは高精度を要求できますが、リアルタイム実行を保証するAPIではありません。Windowsでは他のスレッド、割り込み、電源管理、CPU負荷などの影響を受けるため、通知が遅れることがあります。周期は「処理を開始する目安」と考えるのが適切です。

高い分解能を要求すると、システムが頻繁に起床して消費電力が増える場合があります。必要以上に1ミリ秒周期を指定せず、実際に必要な応答時間と処理量から値を決めます。短周期の通知が必要でも、経過時間をQueryPerformanceCounterなどで測定し、処理側で補正する方法が有効です。

定期処理の基準を「前回の通知から一定時間後」にすると、処理時間が加わって徐々にずれることがあります。絶対時刻を基準に次の実行時刻を決めれば、多少の遅延があっても長期的なドリフトを抑えられます。

現代のWindowsアプリでの選択

timeSetEventは既存のWindowsプログラムやマルチメディア関連のコードで役立ちますが、新規開発では用途に応じて別のタイマーも検討します。GUIの更新ならSetTimer、待機中のスレッド処理なら待機可能タイマー、非同期の定期処理ならスレッドプールタイマーが候補になります。

APIを選ぶときは、必要な精度だけでなく、停止のしやすさ、スレッドモデル、電力消費、例外処理、終了時の安全性も比較します。特にビジネス処理やネットワーク処理では、タイマー通知を直接処理本体にせず、キューへ投入する設計が保守しやすくなります。

// 概念例
void CALLBACK TimerProc(
    UINT, UINT, DWORD_PTR user, DWORD_PTR, DWORD_PTR)
{
    auto* state = reinterpret_cast<State*>(user);
    if (!state->stopping.load()) {
        state->signalWork();
    }
}

このように、コールバックを短く保ち、終了フラグとライフタイムを明確にします。timeSetEventによる定期処理で覚えておくべき点は、指定した周期が厳密な実行保証ではないこと、停止後も実行中のコールバックが残り得ること、そして状態の破棄を通知処理の完了後に行うことです。