Windows待機処理はイベントを使え【15.6msポーリングの罠】

· · Windows開発, 同期, イベント, タイマー, 設計

まず結論

時間を待つなら timer、出来事を待つなら event。

  • Sleep(1) は「1ms 後に正確に起きる」意味ではない。System clock の粒度(約15.6ms)とスケジューリング遅延の影響を受ける
  • 仕事の到着や I/O 完了を待つなら、timer ではなく event を待つほうが遅延・CPU・電力のすべてで有利
  • timeout が過ぎても、thread は「ready になる」だけで即実行は保証されない
  • 時間そのものが条件のときだけ timer を使う

実務での使い分け

待ちたいもの よくない例 まずの選択
キューに仕事が入ること Sleep(1)TryPop event / semaphore
I/O が完了すること timer で状態を見に行く overlapped I/O の event / IOCP
停止要求が来ること 100ms ごとに flag を見る stop event / cancellation
同一プロセス内の値変化 while (flag==0) Sleep(1) WaitOnAddress
時刻が来ること event に無理やり寄せる timer / waitable timer

何が問題なのか

timed wait は system clock の粒度に縛られる

Sleep や timeout 付き wait の精度は、system clock resolution に依存する。指定したミリ秒がそのまま保証されるわけではない。

期限が来てもすぐ実行されない

timeout 後、thread は ready になるが、他の thread や priority、CPU の idle state、DPC/ISR、lock 競合の影響で即実行は保証されない。

Sleep(1) は 1ms 周期のループにならない

while (!g_stop) {
    Step();
    Sleep(1);
}

このループは実際には:

  • Step() の実行時間が毎回加算される
  • Sleep(1) 自体が粒度に引っ張られる
  • 起きてもすぐ走れるとは限らない

なぜイベント待機が有利か

待ちの終了条件が「時間切れ」ではなく「signal」になる

  • timer wait: 何も起きていなくても一定時間で起き、それから「何かあったか」を確認
  • event wait: 何かが起きた側が signal し、signal されたら待ちが満たされる

何を待ちたいかで道具を選ぶ

flowchart LR
    start["待っている thread"] --> q{"待っているものは?"}
    q -- "時間" --> timer["timer / waitable timer"]
    q -- "仕事の到着" --> event["event / semaphore / condition variable"]
    q -- "値の変化" --> addr["WaitOnAddress"]
    q -- "I/O 完了" --> io["completion / event"]
    q -- "停止要求" --> stop["stop event / cancellation"]

典型的なアンチパターンと改善例

アンチパターン: Sleep(1) でキューをポーリング

for (;;) {
    if (g_stop) break;
    WorkItem item;
    if (TryPop(item)) { Process(item); continue; }
    Sleep(1);
}

問題点:

  1. queue が空でも定期的に起きる
  2. latency が timer 粒度に引っ張られる
  3. 電力効率が悪い

改善例: WaitForMultipleObjects

HANDLE waits[2] = { _stopEvent, _workEvent };
for (;;) {
    DWORD rc = WaitForMultipleObjects(2, waits, FALSE, INFINITE);
    if (rc == WAIT_OBJECT_0) return;  // stop
    if (rc == WAIT_OBJECT_0 + 1) DrainQueue();
}

ポイント:

  • Sleep(1) が消えている
  • item 到着時に producer が SetEvent する
  • worker は stopwork を同時に待つ

C#/.NET でも同様の問題が起きる

while (!stoppingToken.IsCancellationRequested) {
    if (_queue.TryDequeue(out var item)) { await ProcessAsync(item); continue; }
    await Task.Delay(1, stoppingToken); // これも本質は polling
}

同一プロセスなら WaitOnAddress も候補

同じプロセス内で単に「ある値が変わるまで待ちたい」だけなら WaitOnAddress が軽量で便利。

  • プロセス間や一般的な待機対象 → event / semaphore / waitable object
  • 同一プロセスの軽い値変化WaitOnAddress

それでも timer を使う場面

時間そのものが条件のときは timer が正しい:

  • 5秒ごとに metrics を送る
  • 200ms 後に retry する
  • 1分ごとにキャッシュを掃除する
  • 期限時刻まで待って timeout にする

この場合は Sleep を雑に積むより waitable timer を使うほうが意味が明確。

timeBeginPeriod を常用しない

精度が気になるからと timeBeginPeriod(1) を足すのは避ける:

  1. power/performance のコストがある
  2. 最近の Windows では挙動が複雑
  3. 根本原因を直していないことが多い

レビュー時のチェックリスト

  • Sleep(1)Task.Delay(1) で様子見ループを作っていないか
  • 本当は queue 到着や I/O 完了を待っているのに timer poll していないか
  • producer 側から signal できる設計になっているか
  • stopwork を 1回の wait でまとめて待てないか
  • 同一プロセスの値変化なら WaitOnAddress で書けないか
  • timer を使っている場所で、本当に待ちたいものが「時間」なのか

まとめ

Windows で短い timer wait を使ったポーリング設計は、timer 粒度と scheduler の影響で見た目ほど正確ではない。

時間を待つなら timer、出来事を待つなら event。 この線引きがはっきりするだけで、latency が読みやすくなり、無駄な periodic wakeup が減り、電力効率も改善し、コードの意図が明確になる。

参考資料

関連する記事

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

未想定例外発生時の対応判断フロー

想定していない例外が発生したとき、アプリを終了すべきか継続すべきかを判断するための実用的な指針をまとめた記事です。失敗単位、共有状態、外部副作用、ネイティブ境界の観点から3段階の選択肢と判断表で整理し、設計判断に直接使えます。

記事を読む

Windowsアプリのセキュリティチェックリスト

WPF・WinForms・WinUI・C++・C# など Windows アプリ開発で最低限外したくないセキュリティ項目を、権限・署名・秘密情報・通信・入力・DLL・ログの観点でチェックリスト化し、リリース前に何を確認すべきか整理した記事です。

記事を読む

FileSystemWatcherの安全な使い方

FileSystemWatcher のイベントは完了通知ではないという前提に立ち、取りこぼし、重複通知、完了判定の落とし穴を整理し、再スキャン要求への畳み込み、原子的 claim、idempotency までの設計指針をまとめます。

記事を読む

関連トピック

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

このテーマがつながるサービス

この記事は次のサービスページにつながります。近い入口からご覧ください。

ブログ一覧に戻る