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を実行したりすると、応答停止や再入が発生する可能性があります。
定期処理の内容が重い場合は、コールバックではイベントを記録してワーカースレッドへ渡す設計が適しています。共有データを変更する場合は、クリティカルセクション、ミューテックス、アトミック操作などを使い、同じ処理が重なって実行されない仕組みを用意します。
コールバックで避けたい処理
- 長時間の計算や大量のファイルアクセス
- GUIオブジェクトの直接操作
- 所有権が不明なポインターへのアクセス
- タイマー停止と同時に破棄される状態への参照
処理時間が周期を超えると、次回通知との重なりや遅延が起こります。コールバックでは最小限の通知だけを行い、実作業を別の実行単位へ移すと、タイマーの安定性を保ちやすくなります。
開始と停止を安全に管理する
タイマーを作成したら、戻り値を必ず保存します。timeKillEvent(timerId)を呼ぶことで定期通知を停止できますが、停止を要求した時点で、すでにコールバックが実行中である可能性があります。そのため、タイマー識別子を消すだけでは終了処理として不十分です。
アプリケーション終了時には、まず新しい通知を止め、コールバックの実行完了を待ち、その後にコンテキストや関連オブジェクトを解放します。コールバック側で参照カウントや終了フラグを使うと、破棄済みメモリーへのアクセスを防ぎやすくなります。
終了処理で確認する項目
timeSetEventの戻り値が0でないかtimeKillEventを一度だけ呼んでいるか- 実行中のコールバックが残っていないか
- ユーザーデータを解放する順序が適切か
- 停止後に処理を再開しない設計か
タイマーをクラスで包む場合は、デストラクターで機械的に解放するだけでなく、コールバックからオブジェクトを参照する経路も管理します。停止処理と破棄処理を同じロックで保護すると、終了時の競合を整理しやすくなります。
精度とシステム負荷の考え方
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による定期処理で覚えておくべき点は、指定した周期が厳密な実行保証ではないこと、停止後も実行中のコールバックが残り得ること、そして状態の破棄を通知処理の完了後に行うことです。