前回のWindows ソフトリアルタイムの実践ガイドでは、Sleep 任せの周期ループを避ける話を書きました。
今回はその中でも、なぜ 短い timer wait より event wait を優先したいのかを 1 点に絞って整理します。
Windows では、Sleep(1) や短い timeout 付きの wait を使って「一定時間ごとに様子を見る」設計をすると、どうしても system clock の粒度と その後のスケジューリング遅延の影響を受けます。
普通の設定では 15.6ms 級の platform timer resolution が前提になることが多いので、「1ms 後にもう一回見よう」というつもりでも、実際にはかなり雑な待ちになりやすいです。
一方で、仕事の到着、I/O 完了、停止要求、状態変化のように、本当に待ちたいものが「時間」ではなく「出来事」なら、一定間隔で見に行く必要はありません。 イベントが起きた側が signal し、待つ側は event を待つほうが、遅延にも CPU にも電力にも素直です。
この記事で答えたい問いは、この 4 つです。
Sleep(1)や短い timer wait が、なぜ思ったより正確でないのか- なぜ event wait はその制限を受けにくいのか
- どういう場面で timer ではなく event を選ぶべきか
- それでも timer を使うべき場面は何か
1. まず結論
- 仕事の到着や I/O 完了を待つなら、timer ではなく event を待つほうがよいです。
- Windows の timed wait は、どうしても system clock の粒度の影響を受けます。
Sleep(1)は「1ms 後に正確に起きる」意味ではありません。- しかも timeout が過ぎても、thread はまず ready になるだけで、即実行は保証されません。
- だから 「本当は出来事を待っているのに、timer で様子を見に行く」設計は、遅延にも電力にも不利です。
- timer を使うのは、本当に時間そのものが条件のときだけに絞ったほうがきれいです。
実務での言い方にすると、ほぼこれです。
- 「5 秒おきに metrics を送る」 -> timer の仕事
- 「キューに仕事が入ったらすぐ動く」 -> event / semaphore / condition variable /
WaitOnAddressの仕事 - 「I/O が終わったら続きを実行する」 -> completion / event の仕事
- 「停止要求が来たら止まる」 -> stop event / cancellation の仕事
2. 何が問題なのか
2.1 timed wait は system clock の粒度に縛られる
Windows の wait functions の timeout 精度は、system clock resolution に依存します。
Sleep も同じで、指定したミリ秒がそのまま「その通りの長さ」で保証されるわけではありません。
ここで大事なのは、1ms を指定したから 1ms 後に起きるとは限らないという点です。
2.2 期限が来ても、すぐ実行されるとは限らない
さらにややこしいのは、timeout が過ぎた瞬間に thread が即実行されるわけではないことです。
Sleep の説明にもある通り、待ち時間が終わったあと thread は readyにはなりますが、今すぐ CPU をもらって走れる保証はありません。
ほかの thread、priority、CPU の idle state、DPC / ISR、lock 競合などの影響を受けます。
つまり、短い timer wait には少なくとも 2 段階の不確実さがあります。
- そもそも timeout の判定自体が timer 粒度に引っ張られる
- timeout 後も、実行開始は scheduler 次第になる
2.3 Sleep(1) は 1ms 周期の意味にならない
Sleep(1) を見ると、つい「1ms ごとに回る loop」っぽく見えます。
でも実際には、そう読んではいけません。
while (!g_stop)
{
Step();
Sleep(1);
}
この loop の実態はこうです。
Step()の実行時間が毎回足されるSleep(1)の待ち時間自体が粒度に引っ張られる- 目が覚めても、すぐ走れるとは限らない
3. なぜイベント待機が有利なのか
3.1 待ちの終了条件が「時間切れ」ではなく「signal」になる
event wait が有利なのは、待ちの意味が変わるからです。
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"]
3.2 何を待ちたいのかで道具を分ける
まずの判断は、だいたいこの表で足ります。
| 待ちたいもの | よくない例 | まずの選択 |
|---|---|---|
| キューに仕事が入ること | Sleep(1) で TryPop する |
event / semaphore |
| I/O が完了すること | timer で状態を見に行く | overlapped I/O の event / IOCP |
| 停止要求が来ること | 100ms ごとに stop flag を見る | stop event / cancellation |
| 同一プロセス内の値変化 | while (flag == 0) Sleep(1) |
WaitOnAddress |
| 時刻が来ること | event に無理やり寄せる | timer / waitable timer |
3.3 event も魔法ではない
event wait は、timer 粒度で起きる必要がないという意味で有利ですが、signal された瞬間に絶対ゼロ遅延で走るわけではありません。
event wait でも、こうした影響は受けます。
- scheduler latency
- thread priority
- CPU の power state
- lock 競合
- page fault
- DPC / ISR
ただし少なくとも、「次の timer tick まで寝ている」という余計な待ち方は外せます。
4. 典型的なアンチパターン
4.1 Sleep(1) でキューをポーリングする
いちばんよく見るのはこれです。
for (;;)
{
if (g_stop)
{
break;
}
WorkItem item;
if (TryPop(item))
{
Process(item);
continue;
}
Sleep(1);
}
この書き方は、一見単純ですが、問題が 3 つあります。
- queue が空でも定期的に起きる
- latency が timer 粒度に引っ張られる
- power 的にも損
4.2 Thread.Sleep(1) / Task.Delay(1) で状態を監視する
C# / .NET でも同じ匂いは出ます。
while (!stoppingToken.IsCancellationRequested)
{
if (_queue.TryDequeue(out WorkItem? item))
{
await ProcessAsync(item, stoppingToken);
continue;
}
await Task.Delay(1, stoppingToken);
}
見た目は async で穏やかでも、設計の本質は polling です。
5. こう直す
5.1 producer が到着時に signal する
queue 到着待ちなら、polling ではなく producer が signal する形に変えます。
- producer が queue に item を入れる
- item を入れた直後に
SetEventする - consumer は
WaitForSingleObjectまたはWaitForMultipleObjectsで待つ - 起きたら queue を drain する
5.2 WaitForMultipleObjects で work と stop を同時に待つ
単純な worker なら、この形が分かりやすいです。
HANDLE waits[2] = { _stopEvent, _workEvent };
for (;;)
{
DWORD rc = WaitForMultipleObjects(2, waits, FALSE, INFINITE);
if (rc == WAIT_OBJECT_0)
{
return;
}
if (rc != WAIT_OBJECT_0 + 1)
{
throw std::runtime_error("WaitForMultipleObjects failed.");
}
DrainQueue();
}
この例のポイントは 3 つです。
Sleep(1)が消えている- item 到着時に producer が
SetEventしている - worker は
stopとworkを同時に待っている
5.3 同一プロセスなら WaitOnAddress も候補
同じプロセス内で、単に「ある値が変わるまで待ちたい」だけなら、WaitOnAddress もかなり有力です。
使い分けの感覚としては、だいたいこうです。
- プロセス間や一般的な待機対象 -> event / semaphore / waitable object
- 同一プロセスの軽い値変化 ->
WaitOnAddress
6. それでも timer を使う場面
6.1 時間そのものが条件のとき
もちろん、timer を使う場面はちゃんとあります。
- 5 秒ごとに metrics を送る
- 200ms 後に retry する
- 1 分ごとにキャッシュを掃除する
- 期限時刻まで待って timeout にする
ここでは待ちたいものが 本当に時間です。
6.2 waitable timer を使う
Windows で「時間そのもの」を待つなら、Sleep を雑に積むより、waitable timer を使ったほうが意味がはっきりします。
6.3 timeBeginPeriod を常用しない
短い timer wait の精度が気になると、つい timeBeginPeriod(1) を足したくなります。
でも、これは常用の第一選択にしないほうがよいです。
理由は 3 つあります。
- power / performance のコストがある
- 最近の Windows では挙動が少し複雑
- 根本原因を直していないことが多い
7. レビュー時のチェックリスト
Sleep(1)/Thread.Sleep(1)/Task.Delay(1)で様子見 loop を作っていないか- 本当は queue 到着、I/O 完了、停止要求を待っているのに timer poll していないか
- producer / completion 側から signal できる設計になっているか
stopとworkを 1 回の wait でまとめて待てないか- 同一プロセスの値変化なら
WaitOnAddressで書けないか - timer を使っている場所で、本当に待ちたいものが「時間」なのか
8. まとめ
Windows で短い timer wait を使って「一定時間ごとに様子を見る」設計は、どうしても timer 粒度と scheduler の影響を受けます。
そのため、Sleep(1) や短い timeout は、見た目ほど正確な待ちではありません。
一方で、仕事の到着、I/O 完了、停止要求、状態変化のように、本当に待ちたいものが「出来事」なら、event wait のほうが自然です。
まとめると、この 1 行に尽きます。
時間を待つなら timer、出来事を待つなら event。
この線引きがはっきりするだけで、
- latency が読みやすくなる
- 無駄な periodic wakeup が減る
- power 的にもましになる
- コードの意図が分かりやすくなる
という形で効いてきます。
9. 参考資料
- Sleep function (Win32)
- Wait Functions
- WaitForSingleObject function
- Event Objects (Synchronization)
- Using Event Objects
- WaitOnAddress function
- WakeByAddressSingle function
- timeBeginPeriod function
- CreateWaitableTimerExW function
- SetWaitableTimer function
- Thread.Sleep Method (.NET)
- Results for the Idle Energy Efficiency Assessment
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
業務システムのコード設計 ── 商品コード・顧客コードの決め方とチェックディジット
商品コード・顧客コードなど業務システムのコード体系を決める実践ガイド。有意コードと無意味連番の判断表、JAN・Luhn等のチェックディジット算式とC#実装、Excelの0落ち対策、桁あふれと移行まで整理します。
ADR(Architecture Decision Record)入門 ── 小規模開発で「なぜこの設計にしたか」を残す最小の方法
コードは「なぜそうしたか」を語りません。ADR(Architecture Decision Record)で設計判断の理由を1決定=1ファイルのMarkdownとして残す方法を、テンプレート、書く/書かないの判断表、実例つきで解説します。
スリープ・休止・Modern Standbyと長時間稼働アプリ ── 「夜中に止まっていた」を設計で防ぐ
長時間動き続けるWindowsアプリが「朝見たら止まっていた」となる原因を、S3スリープ/休止/Modern Standbyの違いから整理します。スリープ中のタイマーやTCP接続の挙動、SetThreadExecutionStateによる抑止まで解説します。
WindowsアプリのWeb化、しない方がいいケース ── 判断表と「分割」という現実解
装置連携・ローカルファイル処理・オフライン運用を抱えたWindowsアプリでは、Web化がコスト増と機能劣化を招くことがあります。Web化に向く・向かないの判断表と、一部だけWebに出す分割構成という現実解を整理します。
想定外例外で終了すべきか継続すべきかの判断表
想定していない例外が起きたときに、アプリを終了させるべきか継続すべきかを、状態破壊・外部副作用・スレッド・ネイティブ境界の観点から整理します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
UI スレッド / タイマーテーマ
WPF / WinForms、UI スレッド、async/await、タイマー設計を整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
技術相談・設計レビュー
待機設計、同期 primitives の選び方、soft real-time での遅延と電力のトレードオフを整理する話なので、技術相談・設計レビューと相性がよいです。
Windowsアプリ開発
Windows アプリやサービスで timer polling を event-driven に置き換える設計は、Windowsアプリ開発の実装品質に直結するテーマです。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- WindowsのSleep(1)はなぜ1ミリ秒後に正確に起きないのですか?
- Windowsのtimed waitのtimeout精度はsystem clock resolutionに依存し、普通の設定では15.6ミリ秒級のplatform timer resolutionが前提になることが多いからです。さらに待ち時間が終わってもthreadはreadyになるだけで、即座にCPUをもらって走れる保証はありません。他のthread、priority、CPUのidle state、DPC/ISR、lock競合などの影響を受けます。つまり短いtimer waitには、timeout判定がtimer粒度に引っ張られる点と、timeout後の実行開始がscheduler次第になる点の、少なくとも2段階の不確実さがあります。
- Sleep(1)やTask.Delay(1)でキューをポーリングする設計の何が問題ですか?
- 問題は3つあります。キューが空でも定期的に起きること、レイテンシがtimer粒度に引っ張られること、電力的にも損なことです。C#のawait Task.Delay(1)ループも見た目は穏やかですが設計の本質はpollingです。直し方は、producerがキューにitemを入れた直後にSetEventし、consumerがWaitForSingleObjectやWaitForMultipleObjectsで待つ形に変えることです。stopイベントとworkイベントを1回のwaitでまとめて待つと、停止要求にもすぐ反応できます。
- タイマー待機とイベント待機はどう使い分ければよいですか?
- 時間を待つならtimer、出来事を待つならevent、というのが線引きです。5秒おきにmetricsを送るような時間そのものが条件の処理はwaitable timerの仕事です。キューへの仕事の到着はeventやsemaphore、I/O完了はoverlapped I/OのeventやIOCP、停止要求はstop eventやcancellation、同一プロセス内の値変化はWaitOnAddressが向いています。この線引きがはっきりすると、レイテンシが読みやすくなり、無駄なperiodic wakeupが減り、コードの意図も分かりやすくなります。
- timeBeginPeriod(1)でタイマー精度を上げれば解決しませんか?
- 常用の第一選択にはしないほうがよいです。理由は3つで、powerとperformanceのコストがあること、最近のWindowsでは挙動が少し複雑になっていること、そして根本原因を直していないことが多いことです。本当に待ちたいものが仕事の到着やI/O完了のような出来事なら、タイマー精度を上げるのではなく、起きた側がsignalするイベント駆動に設計を変えるほうが、遅延にもCPUにも電力にも素直です。
著者プロフィール
記事の著者プロフィールページです。
小村 豪
合同会社小村ソフト 代表
Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。
公開リンク