更新履歴(4件・最終更新 2026年08月02日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- `WaitOnAddress`の例で、フラグを素の変数のまま両スレッドから読み書きしていました。C++ではデータ競合(未定義動作)で、最適化されたビルドでは値がレジスタに載ったまま更新が見えず、起こされても止まり続けることがあります。`WakeByAddressSingle`は待っているスレッドを起こすだけで、直前の書き込みをアトミックにも可視にもしません。書く側をrelease、読む側をacquireで揃えたアトミック変数に直し、一緒に渡すデータの可視性についても書き足しました。
- この記事が単体で読めることと前回記事の結論の要約、用語表を冒頭に追加しました。自分の環境のタイマー粒度を確かめる方法、`WaitForMultipleObjects`の戻り値の扱い(同時にシグナルされたときは添字の小さいほうが返るので停止用を先頭に置く)、`WaitOnAddress`の前提と早期復帰への対処を追加しました。3.1の図は判断表と重複していたため、待ちの終わり方の違いを示す図に差し替えました。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589664)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「WindowsでSleep(1)よりイベント待機を優先すべき理由」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589664 https://staging.comcomponent.com/blog/2026/03/16/006-windows-timer-vs-event-wait/
- DOI(最新版)
- 10.5281/zenodo.21589664
- DOI(この版)
- 10.5281/zenodo.21732692
前回のWindows ソフトリアルタイムの実践ガイドでは、Sleep 任せの周期ループを避ける話を書きました。
今回はその中でも、なぜ 短い timer wait より event wait を優先したいのかを 1 点に絞って整理します。
この記事は単体で読めます。 前回の結論は「Sleep に周期を任せた様子見ループは、待ち時間も起床タイミングも保証されないので、周期処理の土台にしない」の 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 を使うべき場面は何か
この記事で出てくる用語
本文で説明なしに出てくる略語だけ、先に置いておきます。
| 用語 | 意味 |
|---|---|
| platform timer resolution / system clock resolution | OS が時刻を更新する間隔。timed wait の timeout 判定はこの粒度に引っ張られます |
| ISR (Interrupt Service Routine) | 割り込みが起きたときに最優先で走る処理。ここが走っている間、こちらの thread は待たされます |
| DPC (Deferred Procedure Call) | ISR が「あとで続きをやる」ために積む、優先度の高い遅延処理。ISR と合わせて 割り込み処理に伴う遅延要因と読んでおけば、この記事では足ります |
| IOCP (I/O Completion Port) | 非同期 I/O の完了通知をキューにまとめ、専用の thread 群で受け取る Windows の仕組み |
WaitOnAddress |
「あるメモリ番地の値が変わるまで待つ」ための同期 API。同一プロセス内専用です (5.3 で扱います) |
| signal する | 待っている側の条件を満たしてやること。event なら SetEvent を呼ぶことです |
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 の仕事
この記事の知識マップ
Windowsの短いtimer waitはsystem clock resolutionの粒度に縛られるうえ、timeoutが来てもスレッドはready状態になるだけで実行開始はスケジューラ次第になるため、Sleep(1)でキューや停止要求を様子見するポーリング設計は思ったより不正確です。producerがSetEventで知らせ、consumerがWaitForSingleObjectやWaitForMultipleObjectsで待つイベント駆動に変えると、待ちの終わり方が時間切れではなくsignalになり、無駄な空振りが消えます。I/O完了はoverlapped I/OのイベントやIOCP、同一プロセス内の値変化はWaitOnAddress、時刻そのものが条件のときだけwaitable timerを使うというのが道具の選び分けで、timeBeginPeriodによる精度向上は根本的な解決にはなりません。
flowchart LR
accTitle: timerポーリングとイベント待機の知識マップ
accDescr: 短いtimer waitがsystem clock resolutionとスケジューリング遅延の二重の不確実さを持つこと、イベント駆動の待機がキュー到着・I/O完了・停止要求・同一プロセス内の値変化のそれぞれにどう対応するか、waitable timerとWaitOnAddressの使い分けの関係を示す図
timer_polling["timerポーリング(様子見ループ)"]
event_driven_wait["イベント駆動の待機設計"]
system_clock_resolution["system clock resolution(platform timer resolution)"]
scheduler_latency["スケジューリング遅延"]
queue_arrival_wait["キューへの仕事到着待ち"]
windows_event_object["Windowsのイベントオブジェクト"]
wait_functions["Windowsの待機関数(Wait Functions)"]
overlapped_io["Overlapped I/O"]
io_completion_wait["I/O完了待ち"]
iocp["I/O完了ポート(IOCP)"]
stop_request_wait["停止要求待ち"]
waitonaddress["WaitOnAddress API"]
same_process_value_change_wait["同一プロセス内の値変化待ち"]
data_race["データ競合(data race)"]
waitable_timer["待機可能タイマー(waitable timer)"]
time_based_wait["時刻そのものが条件の待ち"]
timebeginperiod["timeBeginPeriod(タイマー分解能要求)"]
getsystemtimeadjustment["GetSystemTimeAdjustment"]
interrupt_processing_delay["割り込み処理に伴う遅延要因(ISR/DPC)"]
timer_polling -->|"前提とする"| system_clock_resolution
timer_polling -.->|"原因になり得る"| scheduler_latency
event_driven_wait -.->|"原因になり得る"| scheduler_latency
timer_polling -->|"用いるのは非推奨"| queue_arrival_wait
windows_event_object -->|"推奨される対応"| queue_arrival_wait
wait_functions -->|"利用する"| windows_event_object
overlapped_io -->|"推奨される対応"| io_completion_wait
iocp -->|"推奨される対応"| io_completion_wait
timer_polling -->|"用いるのは非推奨"| io_completion_wait
windows_event_object -->|"推奨される対応"| stop_request_wait
timer_polling -->|"用いるのは非推奨"| stop_request_wait
wait_functions -->|"推奨される対応"| stop_request_wait
waitonaddress -->|"推奨される対応"| same_process_value_change_wait
waitonaddress -.->|"原因になり得る"| data_race
waitable_timer -->|"推奨される対応"| time_based_wait
timebeginperiod -->|"用いるのは非推奨"| queue_arrival_wait
system_clock_resolution -->|"で確認できる"| getsystemtimeadjustment
interrupt_processing_delay -.->|"原因になり得る"| scheduler_latency
iocp -->|"利用する"| overlapped_io
wait_functions -->|"利用する"| waitable_timer
timer_polling -->|"用いるのは非推奨"| same_process_value_change_wait
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全21件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
2. 何が問題なのか
2.1 timed wait は system clock の粒度に縛られる
Windows の wait functions の timeout 精度は、system clock resolution に依存します。
Sleep も同じで、指定したミリ秒がそのまま「その通りの長さ」で保証されるわけではありません。
ここで大事なのは、1ms を指定したから 1ms 後に起きるとは限らないという点です。
自分の環境の粒度を確かめる
「15.6ms 級」は一般論なので、手元の値は自分で見たほうが早いです。確かめ方は 2 つあります。
1 つは、GetSystemTimeAdjustment を呼ぶことです。第 2 引数の lpTimeIncrement に、system が time-of-day clock を更新する間隔が 100 ナノ秒単位で返ります。15.6ms 級なら 15 万台の値になります。
#include <windows.h>
#include <cstdio>
int main()
{
DWORD adjustment = 0;
DWORD increment = 0;
BOOL adjustmentDisabled = FALSE;
if (!GetSystemTimeAdjustment(&adjustment, &increment, &adjustmentDisabled))
{
std::printf("GetSystemTimeAdjustment failed. GetLastError=%lu\n", GetLastError());
return 1;
}
// increment は 100ns 単位なので、ms に直して見る
std::printf("time increment = %lu (100ns) = %.4f ms\n",
increment,
increment / 10000.0);
return 0;
}
もう 1 つは、Sysinternals の ClockRes を実行することです。これは中で同じ GetSystemTimeAdjustment を呼んで、system clock の分解能、つまりアプリが得られる最大の timer resolution を表示するだけの小さなツールです。コードを書かずに確認したいときはこちらが早いです。
なお、ドキュメント上 lpTimeIncrement は 起動時に system が決める固定値で、稼働中に変わらないものとして説明されています。つまりこれは「この環境の素の粒度」を知るための値であって、timeBeginPeriod を呼んだ結果を測るためのものではありません。そちらについては 6.3 で触れます。
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 TB
subgraph TimerWait["timer wait: 時間が来たから起きる"]
T1["待つ"] --> T2["timer 粒度で起床"]
T2 --> T3{"何か起きていたか?"}
T3 -- "いいえ" --> T1
T3 -- "はい" --> T4["処理する"]
end
subgraph EventWait["event wait: 起きたから起こされる"]
E1["待つ"] --> E2["起きた側が signal する"]
E2 --> E3["起床時点で理由が確定している"]
E3 --> E4["処理する"]
end
timer wait 側にだけ、空振りして戻るループがあります。ここが latency と電力の両方で効いてきます。
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 }; // index 0 = stop, index 1 = work
for (;;)
{
// bWaitAll = FALSE なので、戻り値は「最初に signal された handle の index」
DWORD rc = WaitForMultipleObjects(2, waits, FALSE, INFINITE);
// 失敗は WAIT_FAILED ((DWORD)0xFFFFFFFF)。理由は GetLastError でしか分からない
if (rc == WAIT_FAILED)
{
throw std::system_error(
static_cast<int>(GetLastError()),
std::system_category(),
"WaitForMultipleObjects failed.");
}
if (rc == WAIT_OBJECT_0) // stop
{
return;
}
if (rc == WAIT_OBJECT_0 + 1) // work
{
DrainQueue();
continue;
}
// ここに来るのは INFINITE 待ちでは想定外
// (WAIT_TIMEOUT や WAIT_ABANDONED_0 系)。握りつぶさず落とす
throw std::runtime_error("WaitForMultipleObjects returned an unexpected value.");
}
この例のポイントは 3 つです。
Sleep(1)が消えている- item 到着時に producer が
SetEventしている - worker は
stopとworkを同時に待っている
戻り値の扱いだけ、実務で外しやすい点を補足しておきます。
bWaitAllがFALSEのとき、成功時の戻り値はWAIT_OBJECT_0からWAIT_OBJECT_0 + nCount - 1の範囲で、そこからWAIT_OBJECT_0を引いた値が 配列の index です。== WAIT_OBJECT_0と!= WAIT_OBJECT_0 + 1の 2 分岐だけで書くと、handle を 3 本に増やした瞬間に壊れます- 複数が同時に signal された場合は、index の小さいほうが返ります。上の例で
stopを index 0 に置いているのは、停止要求を取りこぼさないためです - 失敗は例外ではなく
WAIT_FAILED((DWORD)0xFFFFFFFF) という戻り値で返ります。原因はGetLastErrorを呼ばないと分かりません。rc != 期待値をまとめて「失敗」と書いてしまうと、handle が閉じられていた、SYNCHRONIZE権がない、といった原因が消えます - mutex を待ち対象に混ぜるなら
WAIT_ABANDONED_0系も返り得ます。この例では event しか待っていないので想定外として扱っています
5.3 同一プロセスなら WaitOnAddress も候補
同じプロセス内で、単に「ある値が変わるまで待ちたい」だけなら、WaitOnAddress もかなり有力です。
event を作って初期化して、値と同期がずれないように面倒を見る、という手間がなくなります。
使い分けの感覚としては、だいたいこうです。
| 観点 | event / semaphore / waitable object | WaitOnAddress |
|---|---|---|
| 待機対象の範囲 | プロセス間も可。名前付きにできる | 同一プロセス内のみ |
| 起こす側 | SetEvent / ReleaseSemaphore など |
WakeByAddressSingle / WakeByAddressAll |
| 事前準備 | カーネルオブジェクトの生成と handle 管理が要る | 待つ変数があればよい |
| 使えるバージョン | 古くから利用可能 | Windows 8 / Windows Server 2012 以降 |
| リンク | Kernel32.lib |
Synchronization.lib |
使うときに外したくない点が 3 つあります。
- 必ず
WakeByAddressSingleかWakeByAddressAllと対で使います。 値を書き換えた側がこれを呼ばないと、待っている thread は起きません。1 本だけ起こすなら Single、全部起こすなら All です WaitOnAddressは signal されていなくても戻ることがあります。 ドキュメントでも、低メモリ状態などで早く起きる可能性が明記されています。戻ってきたら必ず値をもう一度読んで、本当に変わったかを確かめる while ループで書きます- 待てるサイズは 1 / 2 / 4 / 8 バイトのいずれかです
- フラグはアトミックにします。
WakeByAddressSingleは待っている thread を起こすだけで、直前の書き込みをアトミックにも可視にもしません。素の変数を両側から読み書きすると C++ ではデータ競合(未定義動作)になり、最適化されたビルドでは更新が見えないまま止まり続けることがあります。書く側は release、読む側は acquire で揃えます
// 待つ側と起こす側は別スレッドなので、フラグは必ずアトミックにする。
// 素の ULONG を両側から読み書きすると C++ ではデータ競合(未定義動作)で、
// 最適化されたビルドでは値がレジスタに載ったまま更新が見えず、
// 起こされても止まり続けることがある
std::atomic<ULONG> g_ready{ 0 };
static_assert(std::atomic<ULONG>::is_always_lock_free,
"WaitOnAddress に渡すので、ロックフリーである必要がある");
// 「g_ready が 0 でなくなるまで待つ」最小形
ULONG undesired = 0;
ULONG captured = g_ready.load(std::memory_order_acquire);
while (captured == undesired)
{
// 早期に戻ることがあるので、戻ったら必ず読み直す
WaitOnAddress(&g_ready, &undesired, sizeof(ULONG), INFINITE);
captured = g_ready.load(std::memory_order_acquire);
}
書き換える側は、値を更新したうえで起こします。
// release で書く。こう書くと、この行より前に用意したデータ(下の payload)も、
// acquire で読んだ側から必ず見えます。順序を保証するのはこのストアであって、
// WakeByAddressSingle ではありません ── あれは待っている thread を
// 起こすだけで、直前の書き込みをアトミックにも可視にもしません
g_payload = ...; // 一緒に渡したいデータ
g_ready.store(1, std::memory_order_release);
WakeByAddressSingle(&g_ready);
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
- WaitForMultipleObjects function
- Event Objects (Synchronization)
- Using Event Objects
- WaitOnAddress function
- WakeByAddressSingle function
- WakeByAddressAll function
- GetSystemTimeAdjustment function
- ClockRes - Sysinternals
- 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にも電力にも素直です。