更新履歴(7件・最終更新 2026年08月22日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 追加した図のMermaidソースの字下げを記事内の規約に統一し、レビュー指摘のあった図の表現を本文の記述に合わせて調整しました。本文の文章は変えていません。
- レビュー指摘に対応し、今日追加した図のうち幅が大きすぎたものを縦向きの構成に直し、一部の図とキャプションの表現を本文の記述に合わせて正確にしました。本文の文章は変えていません。
- 本文の流れ・対比を図でも追えるように、Mermaid図を17点追加しました(地の文500〜750字につき1図の規約に合わせたものです)。既存の判断フロー図にはキャプションを追加しています。本文の文章は変えていません。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- サンプルリポジトリの実在するテストからの抜粋を追加しました(コールバックの重なりと、放置後のtickの畳み込み)。`DispatcherPriority.Background`が既定であることの説明、WinFormsのタイマーは精度が55ミリ秒程度であること、`async void`相当の例外がスレッドプール上ではプロセスごと落とすことを追加しました。
- 本文中の関連記事へのリンクの文言が、リンク先の現在のタイトルと食い違っていたのを、実際のタイトルに揃えました。本文の内容は変えていません。
- 初版公開
この記事を引用する(DOI(登録済みアーカイブ): 10.5281/zenodo.21589623)
以下のDOIは過去に登録されたアーカイブを指しており、現在の本文とは一致しない場合があります。現在の本文を参照するときは、このページのURLを使用してください。
小村 豪(2026)「.NETタイマー3種の使い分け - PeriodicTimer/Timer/DispatcherTimer」合同会社小村ソフト. https://comcomponent.com/blog/2026/03/12/002-periodictimer-system-threading-timer-dispatchertimer-guide/
- DOI(登録済みアーカイブ)
- 10.5281/zenodo.21589623
- DOI(前回登録した版)
- 10.5281/zenodo.21732650
前回の 普通のWindowsでソフトリアルタイムをできるだけ実現するための実践ガイド - まず見るチェックリスト では、Sleep 任せの周期ループを避け、イベント駆動や waitable timer を使う話を整理しました。ひとことで言えば、周期の揺れと deadline miss を減らしたいなら、タイマーの種類より先に「待ち方」そのものを設計する、というのが前回の結論です。
では、もっと普段の .NET アプリ開発ではどうするのか。
ここで迷いやすいのが、PeriodicTimer、System.Threading.Timer、DispatcherTimer です。
名前はどれもタイマーですが、
awaitでティックを待つタイマー- ThreadPool で callback が飛んでくるタイマー
- UI スレッドの
Dispatcher上で動くタイマー
というように、性格がかなり違います。
flowchart TB
accTitle: 名前は似ていても性格が違う3つのタイマー
accDescr: PeriodicTimerはawaitでティックを待つタイマー、System.Threading.TimerはThreadPoolでcallbackが飛んでくるタイマー、DispatcherTimerはUIスレッドのDispatcher上で動くタイマーという性格の違いを示す。
t["3つのタイマー"] --> pt["PeriodicTimer: awaitで待つ"]
t --> st["Timer: callbackが飛んでくる"]
t --> dt["DispatcherTimer: UIスレッド"]
図1: 名前はどれもタイマーだが、待ち方と動く場所の性格がまったく違う。
実務で混ざりやすいのは、だいたいこのへんです。
- 非同期の定期処理なのに
System.Threading.Timerにasyncラムダを渡してしまう - WPF の UI 更新なのに ThreadPool タイマーから直接画面を触ってしまう
DispatcherTimerに重い処理を入れて、画面ごと鈍らせる- 前回の「ソフトリアルタイム」の話と、普段のアプリの定期実行が頭の中で混ざる
この記事では、主に .NET 6 以降の一般的な C# / .NET アプリを前提に、
PeriodicTimer / System.Threading.Timer / DispatcherTimer を、普段の実務で迷いにくい順番で整理します。
対象として想定しているのは、このあたりです。
- worker / バックグラウンドサービス
- コンソールアプリ
- ASP.NET Core の裏方処理
- WPF のデスクトップアプリ
この記事でいう DispatcherTimer は、主に WPF の System.Windows.Threading.DispatcherTimer を指します。
WinUI / UWP にも同じ考え方の DispatcherTimer があります。
WinForms なら、UI 用タイマーとしては System.Windows.Forms.Timer を見るほうが自然です。
この記事は UI 側の説明を WPF に寄せていますが、WinForms の方も対象に含めています。DispatcherTimer を System.Windows.Forms.Timer に読み替えれば、4.3 と 5.2 の注意はそのまま当てはまります。そのうえで、次の 2 点だけ違います。
System.Windows.Forms.Timerはメッセージループ経由で Tick が起きるシングルスレッドのタイマーで、DispatcherTimerのような優先度(DispatcherPriority)の指定はありません- Microsoft のドキュメントで 精度は 55 ミリ秒程度が限界 とされています。細かい周期には向かないので、その場合は UI 用タイマー以外を検討します
なお、ここで扱うのは アプリ側の定期実行をどう書くか です。 周期の正確さそのものが主題 のときは、前回のソフトリアルタイム記事の話に戻ります。
flowchart TB
accTitle: この記事の守備範囲
accDescr: アプリ側の定期実行をどう書くかがこの記事の範囲で、周期の正確さそのものが主題のときは待ち方を設計するソフトリアルタイム記事の話に戻るという切り分けを示す。
q{"何が主題か"}
q -->|"アプリの定期実行の書き方"| here["この記事の3タイマーの話"]
q -->|"周期の正確さそのもの"| rt["待ち方を設計する前回の話"]
図2: 「一定間隔で何かする」でも、定期実行の書き方と周期精度の設計は別の問題。
また、この記事に登場するコードは、ビルド・実行できるサンプル一式(PeriodicTimer / System.Threading.Timer のライブラリとコンソールデモ、tick の畳み込みや callback の重なりを検証するユニットテスト)として GitHub で公開しています。
periodictimer-system-threading-timer-dispatchertimer-guide - komurasoft-blog-samples (GitHub)
目次
- まず結論(ひとことで)
- まず一枚で整理
- 2.1. 全体像
- 2.2. まずの判断表
- まず区別したいこと
- 3.1. callback 型か、tick を待つ型か
- 3.2. ThreadPool で動くのか、UI スレッドで動くのか
- 3.3. 周期処理と精度保証は別の話
- 典型パターン
- 4.1. async な定期処理なら、
PeriodicTimer - 4.2. 軽い callback を ThreadPool で回すなら、
System.Threading.Timer - 4.3. WPF の UI 更新なら、
DispatcherTimer - 4.4. ソフトリアルタイム寄りの周期処理なら、別の道具を見る
- 4.1. async な定期処理なら、
- よくあるアンチパターン
- レビュー時のチェックリスト
- ざっくり使い分け
- まとめ
- 参考資料
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全18件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
1. まず結論(ひとことで)
awaitベースで一定間隔の処理を自然に書きたいなら、まずPeriodicTimer- ThreadPool 上で軽い callback を定期的に起動したいなら、
System.Threading.Timer - WPF の UI スレッドで画面更新したいなら、
DispatcherTimer System.Threading.Timerは callback が重なりうる。非同期処理を雑に突っ込むと荒れやすいDispatcherTimerは UI を直接触れる代わりに、重い処理を入れると UI ごと止めやすい- 前回のソフトリアルタイムの文脈では、この 3 つは高精度待機の主役ではない
要するに、最初に見るべきなのは次の 3 つです。
- どのスレッド / コンテキストで動かしたいか
- 処理本体を
async/awaitで直列に書きたいか - callback の重なりを許せるか
この 3 つを分けるだけで、かなり迷いにくくなります。
flowchart TB
accTitle: 最初に見る3つの問い
accDescr: どのスレッドやコンテキストで動かしたいか、処理本体をasync/awaitで直列に書きたいか、callbackの重なりを許せるかの3つを分けるだけで迷いにくくなる。
q1["どこで動かしたいか"] --> pick["タイマー選びが定まる"]
q2["asyncで直列に書きたいか"] --> pick
q3["callbackの重なりを許せるか"] --> pick
図3: タイマー名より先に、この3つの問いを分けて考える。
2. まず一枚で整理
2.1. 全体像
flowchart LR
A["一定間隔で何かしたい"] --> B{"UIスレッドで動かしたい?"}
B -- "はい" --> C["DispatcherTimer"]
B -- "いいえ" --> D{"処理本体を<br/>async / await で<br/>素直に書きたい?"}
D -- "はい" --> E["PeriodicTimer"]
D -- "いいえ" --> F{"ThreadPool で<br/>軽い callback を<br/>回したい?"}
F -- "はい" --> G["System.Threading.Timer"]
F -- "いいえ" --> H["別の設計も検討<br/>Channel / BackgroundService / event / waitable timer"]
図4: UIスレッドか、asyncで書きたいか、軽いcallbackかの順に切ると3タイマーが決まる。
実務では、だいたいこの分岐で十分です。迷ったときにいちばん外しにくいのは、
非同期処理なら PeriodicTimer、UI 更新なら DispatcherTimer と先に切ることです。
System.Threading.Timer は便利ですが、callback の重なりや寿命管理の癖があるので、
最初の 1 本目としては少し気難しいです。
2.2. まずの判断表
| 状況 | まずの選択 | 実行される場所 | 向いている理由 | まずの注意点 |
|---|---|---|---|---|
| 一定間隔で HTTP / DB / ファイル I/O などの async 処理を回したい | PeriodicTimer |
今の async メソッドの流れの中 | await ベースで書けて、停止とキャンセルが素直 |
1 タイマー 1 コンシューマー前提。遅れは勝手に並列化されない |
| 軽い heartbeat / メトリクス送信 / キャッシュの期限切れチェックを ThreadPool で回したい | System.Threading.Timer |
ThreadPool | 軽量で callback 型。既存の callback ベース設計に載せやすい | callback は再入可能前提。重なりうる。参照を保持する |
| WPF の時計表示や軽い UI 更新を一定間隔で回したい | DispatcherTimer |
WPF の Dispatcher(UI スレッド) |
UI をそのまま触れる。優先度を持てる | 正確な発火時刻は保証されない。重い処理で UI が詰まる |
周期の正確さが本体で、Sleep 任せを避けたい |
この 3 つを主役にしない | - | 目的がアプリの定期実行ではなく、待機精度の設計になる | event / waitable timer 側を見る |
この表で大事なのは、タイマー名より、実行場所と書き方を見る ことです。 タイマー選びで事故るときは、API 名称より「どこで走るか」を見ていないことのほうが多いです。
flowchart TB
accTitle: タイマー選びの着眼点
accDescr: タイマー選びで事故るのはAPIの名前で選んでいるときで、どこで走るかという実行場所と、どう書きたいかという書き方を見れば外しにくくなる。
name["名前で選ぶ"] --> miss["どこで走るかを見ず事故る"]
look["実行場所と書き方で選ぶ"] --> hit["外しにくい選択になる"]
図5: 事故の多くは名前選び。見るべきは「どこで走るか」と「どう書きたいか」。
3. まず区別したいこと
3.1. callback 型か、tick を待つ型か
ここを分けると、一気に見通しがよくなります。
System.Threading.TimerとDispatcherTimerは callback / event 型PeriodicTimerは tick をawaitして待つ型
つまり、
- callback 型は「タイマー側が呼んでくる」
PeriodicTimerは「こちらが次の tick を待つ」
という違いです。
処理本体が async で、
「待つ → 処理する → また待つ」を 1 本の流れとして読みたいなら、PeriodicTimer のほうが自然です。
逆に、
- 既存の callback ベース設計に載せたい
- 処理本体が短くて同期的
- 単純に定期キックしたい
という場面では、System.Threading.Timer が合います。
PeriodicTimer は便利ですが、万能ではありません。
1 つのタイマーに対して同時に複数の WaitForNextTickAsync を飛ばす前提ではなく、
待っていない間に複数回 tick しても、それは 1 回に畳まれます。
ここを「勝手に追いついてくれる」と誤解しないのが大事です。
flowchart TB
accTitle: callback型とtickを待つ型
accDescr: System.Threading.TimerとDispatcherTimerはタイマー側が呼んでくるcallback型で、PeriodicTimerはこちらが次のtickをawaitで待つ型という違いを示す。
q{"どちらの型か"}
q -->|"callback型"| cb["タイマー側が呼んでくる"]
q -->|"tickを待つ型"| tick["こちらが次のtickを待つ"]
cb --> cbex["TimerとDispatcherTimer"]
tick --> ptex["PeriodicTimer"]
tick -.-> flow["待つ→処理→また待つが1本に"]
図6: 「呼ばれる」か「待つ」か。この区別だけで見通しが一気によくなる。
3.2. ThreadPool で動くのか、UI スレッドで動くのか
次に見るべきなのは、どこで実行されるか です。
System.Threading.Timer の callback は、作成したスレッドではなく ThreadPool で動きます。
このため、バックグラウンド処理には向く一方、UI を直接触る前提ではありません。
一方で DispatcherTimer は、Dispatcher キューに統合された UI 用タイマーです。
WPF では、同じ Dispatcher 上で動くので、Tick ハンドラの中で UI をそのまま更新できます。
この違いはかなり大きいです。
- ThreadPool タイマーから UI を触るには、明示的に UI へ戻す必要がある
DispatcherTimerは UI を触りやすいが、その分 UI スレッドの時間を使う
つまり、DispatcherTimer は「UI を安全に触れる」ことが強みですが、
それは同時に「重い処理を入れると入力や再描画も巻き込む」という意味でもあります。
flowchart TB
accTitle: 実行場所の違いと引き換え
accDescr: System.Threading.TimerのcallbackはThreadPoolで動くためUIを触るには明示的に戻す必要があり、DispatcherTimerはUIをそのまま触れる代わりにUIスレッドの時間を使う。
st["Timer: ThreadPoolで動く"] --> back["UIを触るには明示的に戻す"]
dt["DispatcherTimer: UIスレッド"] --> easy["UIをそのまま触れる"]
easy --> cost["重い処理は入力や描画を巻き込む"]
図7: どこで実行されるかの違いは、UIの触りやすさとUIスレッドの消費という引き換えになる。
3.3. 周期処理と精度保証は別の話
ここは前回の記事とのつながりとして大事です。
一定間隔で何かする、という言い方は同じでも、
- アプリの都合として、数秒おきに定期処理したい
- 1ms〜数ms 級で、できるだけ deadline に近づけたい
は、別の問題です。
System.Threading.Timer は軽量で扱いやすいタイマーですが、
精度のための専用道具ではありません。
DispatcherTimer も、Dispatcher キューの都合や優先度の影響を受けます。
PeriodicTimer も名前だけ見ると「周期がきっちりしていそう」に見えますが、
実務での強みは precision というより async フローの書きやすさ です。
なので、
- アプリの定期実行 を書きたいのか
- 待機精度 を詰めたいのか
は、最初に分けたほうが安全です。
この 2 つが混ざると、タイマー選びの議論がだんだん変な方向へ行きます。
flowchart TB
accTitle: 定期実行と待機精度の切り分け
accDescr: 数秒おきの定期処理というアプリの都合と、ミリ秒級でdeadlineに近づけたいという待機精度の問題は別で、3つのタイマーは精度のための専用道具ではない。
same["一定間隔で何かしたい"] --> a["アプリの定期実行"]
same --> b["待機精度を詰めたい"]
a --> timers["3つのタイマーの出番"]
b --> design["待ち方そのものの設計へ"]
b -.-> note["3タイマーは精度の専用道具でない"]
図8: 「一定間隔」という言い方が同じでも、定期実行と精度保証は別の問題として扱う。
4. 典型パターン
4.1. async な定期処理なら、PeriodicTimer
worker や BackgroundService、コンソールの常駐処理などで、
一定間隔で async な処理を回したいなら、まず PeriodicTimer が書きやすいです。
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public sealed class CacheRefreshWorker : BackgroundService
{
private readonly ILogger<CacheRefreshWorker> _logger;
public CacheRefreshWorker(ILogger<CacheRefreshWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("CacheRefreshWorker started.");
await RefreshCacheAsync(stoppingToken);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
try
{
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await RefreshCacheAsync(stoppingToken);
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("CacheRefreshWorker stopping.");
}
}
private async Task RefreshCacheAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Refreshing cache...");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
この形のよいところは、
- コードの流れが 1 本の
asyncメソッドとして追いやすい CancellationTokenをそのまま下流へ渡しやすい- callback ベースの寿命管理や例外管理を減らせる
特に、処理本体が
- HTTP を呼ぶ
- DB に問い合わせる
- ファイルを読む
- 他の async API を await する
のような I/O 待ち中心 なら、かなり相性がよいです。
注意点は 2 つです。
- 1 タイマー 1 コンシューマー前提で使う
- 処理時間が周期より長いときの方針を、自分で決める
PeriodicTimer は、前の処理が長引いたからといって、自動で並列化して追いついてくれるわけではありません。
その意味では、「一定間隔の async ループを自然に書く」ためのタイマーです。
テストしやすさまで見るなら、TimeProvider を受けるコンストラクターを使えるのも地味に便利です。
flowchart TB
accTitle: PeriodicTimerによる定期ループの流れ
accDescr: WaitForNextTickAsyncで次のtickを待ち、asyncの処理本体を実行してまた待つという1本の流れで書け、CancellationTokenのキャンセルでループを抜ける。
wait["WaitForNextTickAsyncで待つ"] --> work["asyncの処理本体を実行"]
work --> wait
wait -.->|"tokenのキャンセル"| exitl["ループを抜けて停止"]
work -.-> note["遅れても自動で並列化されない"]
図9: PeriodicTimerは「待つ→処理する→また待つ」を1本のasyncメソッドとして書ける。
4.2. 軽い callback を ThreadPool で回すなら、System.Threading.Timer
定期的に短い callback を呼びたいだけなら、System.Threading.Timer は素直です。
たとえば、
- heartbeat を打つ
- 軽いメトリクスを採る
- 短い期限切れチェックを入れる
- 既存の callback ベース設計にぶら下げる
といった場面です。
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public sealed class HeartbeatService : IHostedService, IDisposable
{
private readonly ILogger<HeartbeatService> _logger;
private Timer? _timer;
private int _running;
public HeartbeatService(ILogger<HeartbeatService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(OnTimer, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void OnTimer(object? state)
{
if (Interlocked.Exchange(ref _running, 1) != 0)
{
return;
}
try
{
_logger.LogInformation("Heartbeat: {Now}", DateTimeOffset.Now);
}
finally
{
Volatile.Write(ref _running, 0);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
この例で Interlocked.Exchange を入れているのは、
System.Threading.Timer が 前回の callback 完了を待たない からです。
ここはかなり大事です。
- callback は ThreadPool で動く
- callback は再入可能前提
- 間隔より処理が長ければ、重なりうる
この「重なりうる」は、サンプルのユニットテストで実際に観測できる形にしてあります。周期 50ms のタイマーに 300ms かかる処理を入れ、同時実行数の最大値を数えると 2 以上になります。上と同じ Interlocked.Exchange のガードを入れた版では、同じ条件でも同時実行数の最大値は 1 のままで、代わりに実行中に発火した callback がスキップされます。
// tests/KomuraSoft.TimerSelection.Tests/ThreadPoolTimerOverlapTests.cs より抜粋
// ガードなし: 周期 50ms に対して処理 300ms。重なりが観測されるまで待つ
bool overlapped = await WaitUntilAsync(
() => Volatile.Read(ref maxObserved) >= 2,
TimeSpan.FromSeconds(10));
Assert.True(overlapped, "timer callbacks did not overlap within the timeout.");
// ガードあり: 同じ条件でも同時実行数の最大値は 1 のまま
Assert.Equal(1, Volatile.Read(ref maxConcurrent));
処理が軽くない場合は、
- 重複起動をスキップする
- キューへ積む
PeriodicTimerに寄せる
のように設計したほうが穏やかです。
flowchart TB
accTitle: callbackの重なりとガード
accDescr: System.Threading.Timerは前回のcallback完了を待たないため、間隔より処理が長いと重なりうる。Interlocked.Exchangeのガードを入れると同時実行は1のままになり、実行中に発火したcallbackはスキップされる。
fire["間隔ごとにcallback発火"] --> q{"前回がまだ実行中?"}
q -->|"ガードなし"| overlap["callbackが重なって走る"]
q -->|"ガードあり"| skip["今回の発火をスキップ"]
skip --> one["同時実行は1のまま"]
図10: 前回の完了を待たないタイマーでは、重なりを許すかガードで弾くかを自分で決める。
もう 1 つ地味に大事なのは、参照を保持すること です。
System.Threading.Timer は動作中でも、参照がなくなると GC の対象になります。
また、Dispose() を呼んだ直後でも、すでにキューされた callback が後から走ることがあります。
つまり System.Threading.Timer は、
- 軽量
- 速い
- シンプル
ですが、その代わりに callback の都合をこちらがきちんと受け持つタイマーです。
flowchart TB
accTitle: System.Threading.Timerの寿命の注意
accDescr: System.Threading.Timerは動作中でも参照がなくなるとGCの対象になり、Disposeを呼んだ直後でもすでにキューされたcallbackが後から走ることがある。
t["System.Threading.Timer"] --> ref["参照を保持し続ける"]
ref -.-> gc["参照が切れるとGC対象に"]
t --> disp["Disposeで片付ける"]
disp -.-> late["キュー済みcallbackは後から走りうる"]
図11: 軽量な代わりに、参照の保持とDispose後のcallbackまでこちらが受け持つ。
4.3. WPF の UI 更新なら、DispatcherTimer
WPF で画面上の時計や軽い状態表示を定期更新したいなら、DispatcherTimer が自然です。
using System;
using System.Windows;
using System.Windows.Threading;
public partial class MainWindow : Window
{
private readonly DispatcherTimer _clockTimer;
public MainWindow()
{
InitializeComponent();
_clockTimer = new DispatcherTimer(DispatcherPriority.Background)
{
Interval = TimeSpan.FromSeconds(1)
};
_clockTimer.Tick += ClockTimer_Tick;
_clockTimer.Start();
}
private void ClockTimer_Tick(object? sender, EventArgs e)
{
ClockText.Text = DateTime.Now.ToString("HH:mm:ss");
}
protected override void OnClosed(EventArgs e)
{
_clockTimer.Stop();
_clockTimer.Tick -= ClockTimer_Tick;
base.OnClosed(e);
}
}
コンストラクターに渡している DispatcherPriority.Background は、Tick を Dispatcher キューのどの優先度で処理するかの指定です。引数なしの new DispatcherTimer() も既定で Background になるので、ここは既定値を明示しているだけで、挙動を変えているわけではありません。Background(値 4)は「他の非アイドル処理がすべて終わったあとに処理する」優先度で、Input(5)や Render(7)より低い位置にあります。つまり、入力の処理や描画を押しのけてまで Tick を走らせない、ということです。時計表示のように「多少ずれても構わないが、操作の邪魔はしたくない」用途とはよく合います。Tick の結果をできるだけ早く画面へ出したいなら Normal(9)まで上げる選択もありますが、その場合は Tick の中身を軽く保つ前提がより強くなります。
flowchart TB
accTitle: DispatcherPriorityの位置づけ
accDescr: Backgroundは他の非アイドル処理がすべて終わったあとに処理する優先度で、InputやRenderより低く、入力や描画を押しのけてTickを走らせない。早く画面へ出したいならNormalまで上げる選択もある。
tick["DispatcherTimerのTick"] --> bg["Background〔値4〕で積む"]
bg --> after["入力や描画の後に処理される"]
after -.-> fit["操作の邪魔をしない時計向き"]
bg -.-> up["急ぐならNormal〔値9〕へ"]
up -.-> cond["その分Tickは軽く保つ前提"]
図12: 既定のBackground優先度は「入力や描画を押しのけない」位置にTickを積む。
DispatcherTimer のよいところは、Tick が WPF の Dispatcher 上で処理されるため、UI をそのまま触れることです。
これはたとえば、
- 時計表示
- 接続状態の軽い表示更新
- Command の再評価きっかけ
- 画面に出ている数値の軽い更新
のような場面と相性がよいです。
ただし、ここでも空気が変わる点があります。
DispatcherTimer は UI スレッドで動く ので、
Tick ハンドラで重い処理をすると、そのまま入力・描画・再配置まで巻き込んで遅くなります。
また、DispatcherTimer は「指定時刻ぴったり」を保証する道具ではありません。
Dispatcher キュー上の他の仕事や優先度の影響を受けます。
なので実務では、
- Tick の中身は軽くする
- 重い I/O や CPU は別へ逃がす
- 閉じるときは
Stop()と購読解除をして寿命を明示する
くらいまで意識しておくと安定します。
flowchart TB
accTitle: DispatcherTimerを安定させる3点
accDescr: Tickの中身は軽くし、重いI/OやCPU処理は別へ逃がし、画面を閉じるときはStopと購読解除で寿命を明示するという3点で安定する。
dt["DispatcherTimerの運用"] --> l1["Tickの中身は軽くする"]
dt --> l2["重い仕事は背景へ逃がす"]
dt --> l3["Stopと購読解除で寿命を明示"]
l1 -.-> why["UIスレッドの時間を使うため"]
図13: UIをそのまま触れる便利さの分、Tickを軽く保ち寿命を明示する運用が要る。
4.4. ソフトリアルタイム寄りの周期処理なら、別の道具を見る
ここが前回の記事との接続点です。
前回のソフトリアルタイム記事で扱ったのは、 「何秒おきにだいたい動けばよい」ではなく、 周期の揺れや deadline miss をどう減らすか という話でした。
その文脈では、
Sleep任せの相対待機にしない- イベント駆動や waitable timer を使う
- fast path と slow path を分ける
- 遅れを計測する
が主題になります。
なので、
- 普段のアプリの async な定期処理
→
PeriodicTimer - ThreadPool callback
→
System.Threading.Timer - UI 更新
→
DispatcherTimer - 周期精度そのものが主役 → 前回の記事の世界
というふうに、最初から問題を分けてしまうのがきれいです。
「1ms ごとにできるだけきっちり回したい。どの .NET タイマーがよいか」という問いは、 半分くらいはもうタイマー選びではなく、待機方法と設計の問題です。
flowchart TB
accTitle: 問題を最初から4つに分ける
accDescr: 普段のアプリのasyncな定期処理はPeriodicTimer、ThreadPool callbackはSystem.Threading.Timer、UI更新はDispatcherTimer、周期精度そのものが主役なら待機方法の設計というソフトリアルタイム側の話に分ける。
p["やりたいこと"] --> a["async: PeriodicTimer"]
p --> b["callback: Timer"]
p --> c["UI: DispatcherTimer"]
p --> d["精度: 待機方法の設計"]
図14: 「どのタイマーか」の前に、そもそもどの問題かを4つに分けてしまうのがきれい。
5. よくあるアンチパターン
5.1. System.Threading.Timer に async ラムダをそのまま渡す
これはかなりやりがちです。
_timer = new Timer(async _ => await RefreshAsync(), null,
TimeSpan.Zero, TimeSpan.FromSeconds(5));
見た目はすっきりしていますが、TimerCallback は void です。
つまりこの async ラムダは、実質 async void 的な扱いになります。
すると、
- 呼び出し側が await できない
- 完了を待てない
- 例外管理が難しい
- callback の重なりも別途考える必要がある
という、扱いにくい状態になります。
例外管理が難しい理由は、もう少しはっきり書いておく価値があります。async Task なら例外は Task に載るので、呼び出し側が await した時点で受け取れます。async void(相当)にはその Task が無いので、投げられた例外は そのメソッドが開始したときに有効だった SynchronizationContext へ直接投げ直されます。System.Threading.Timer の callback は ThreadPool 上で動き、そこに SynchronizationContext はありません。結果として、例外は ThreadPool のスレッド上の未処理例外になり、既定ではプロセスごと落ちます。callback の中を try / catch で自前に包まない限り、外側では受け止められません。
flowchart TB
accTitle: asyncラムダをcallbackに渡した例外の行き先
accDescr: TimerCallbackはvoidなので渡したasyncラムダはasync void相当になり、例外は開始時のSynchronizationContextへ投げ直されるが、ThreadPool上にはそれが無いため未処理例外となり既定ではプロセスごと落ちる。
ex["asyncラムダ内の例外"] --> void["async void相当で外へ抜ける"]
void --> ctx{"SynchronizationContextは?"}
ctx -->|"ThreadPool上には無い"| unh["ThreadPoolの未処理例外に"]
unh --> crash["既定ではプロセスごと落ちる"]
ex -.-> guard["自前のtry / catchでしか防げない"]
図15: 見た目がすっきりしたasyncラムダの例外は、受け止める場所がないままプロセスを落とす。
処理本体が async なら、まず PeriodicTimer を検討したほうが読みやすいです。
5.2. DispatcherTimer の Tick に重い処理を入れる
DispatcherTimer は UI をそのまま触れるので、つい何でも書きたくなります。
でも、そこは UI スレッドです。
- 長い同期処理
- 重い CPU 計算
- ブロッキング I/O
- 長い
awaitを含む二重起動しうる処理
を入れると、UI の入力や描画と正面衝突します。
Tick の中身は軽くして、 重い仕事は背景に逃がし、必要な結果だけ UI に戻すほうが安定します。
5.3. PeriodicTimer なら遅れを自動で取り戻してくれると思う
ここも誤解しやすいです。
PeriodicTimer は、一定間隔の async ループをきれいに書く道具としては優秀ですが、
前の処理が長引いたときに、勝手に並列実行して追いついてくれるわけではありません。
これはサンプルのユニットテストでも確認できます。周期 250ms のタイマーを、誰も待っていない状態で 1.5 秒放置してから待つと、溜まっていたぶんで 1 回目の待機は即完了しますが、2 回目は即完了しません。放置した間の複数回ぶんの tick が、1 回に畳まれているということです。
// tests/KomuraSoft.TimerSelection.Tests/PeriodicTimerBehaviorTests.cs より抜粋
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(250));
await Task.Delay(TimeSpan.FromMilliseconds(1500)); // この間、誰も待っていない
// 1 回目の待機は、溜まっていた tick によって即完了する
ValueTask<bool> first = timer.WaitForNextTickAsync();
Assert.True(first.IsCompleted);
Assert.True(await first);
// 2 回目の待機は即完了しない(複数回ぶんの tick は残っていない)
ValueTask<bool> second = timer.WaitForNextTickAsync();
Assert.False(second.IsCompleted);
待っていない間の tick が 1 回に畳まれることもあるので、
- 遅れたらスキップするのか
- 最新だけ見ればよいのか
- 必ず全回数ぶん処理したいのか
は、設計で決める必要があります。
flowchart TB
accTitle: 待っていない間のtickの畳み込み
accDescr: PeriodicTimerを誰も待っていない間に複数回tickしても1回に畳まれるため、遅れたらスキップするのか、最新だけ見るのか、全回数ぶん処理したいのかは設計で決める必要がある。
idle["誰も待っていない間に複数tick"] --> fold["1回に畳まれる"]
fold --> q{"遅れの扱いは?"}
q --> s1["スキップする"]
q --> s2["最新だけ見る"]
q --> s3["全回数ぶん処理する"]
図16: 溜まったtickは1回に畳まれる。追いつき方は自動ではなく設計で決める。
5.4. 停止と寿命管理を後回しにする
タイマーは、動かすより止めるほうが事故ります。
見落としやすいのは、このあたりです。
System.Threading.Timerをローカル変数のまま作って参照を保持しないSystem.Threading.Timerを止めずにDispose()まわりを曖昧にするDispatcherTimerをStop()せず、Tick 購読も外さない- 画面を閉じたあとも、タイマーがオブジェクト寿命を引っ張る
特に DispatcherTimer は、メソッドがバインドされているオブジェクトを生かし続けることがあります。
「なんかこの Window、閉じたはずなのに残ってるな」という妙な感じが出たら、ここを疑いたくなります。
flowchart TB
accTitle: 停止と寿命管理の落とし穴
accDescr: 参照を保持しないTimerや曖昧なDisposeは止め方が曖昧なまま残る見落としであり、StopもTick購読解除もしないDispatcherTimerはバインド先のオブジェクトを生かし続けて、閉じたはずのWindowが残る形で現れる。
m1["Timerの参照を保持しない"] --> trouble["止め方が曖昧なまま"]
m2["Disposeまわりが曖昧"] --> trouble
m3["StopもTick購読解除もしない"] --> keep["バインド先を生かし続ける"]
keep --> ghost["閉じたはずのWindowが残る"]
図17: タイマーは動かすより止めるほうが事故る。寿命の後始末を最初から書いておく。
6. レビュー時のチェックリスト
- その周期処理は、UI 更新 / ThreadPool callback / async ループ のどれとして書くべきか説明できるか
- 処理本体が async なのに、callback 型タイマーへ無理に押し込んでいないか
System.Threading.Timerを使うなら、callback の重なりに耐えられるか、またはガードしているかDispatcherTimerの Tick に重い処理、ブロッキング I/O、長い同期処理を入れていないかPeriodicTimerを使うなら、遅れたときの方針が決まっているか- 停止方法 (
Change/Dispose/Stop) と、アプリ終了時の流れが明確か System.Threading.Timerの参照をちゃんと保持しているかDispatcherTimerの購読解除や画面クローズ時の後始末があるか- その問題が「アプリの定期実行」なのか「待機精度」なのか、最初に分けられているか
7. ざっくり使い分け
実務での目安を挙げておきます。
-
30 秒おきに API を叩いて設定を更新したい →
PeriodicTimer -
5 秒おきに heartbeat や軽いメトリクスを送りたい →
System.Threading.Timer -
WPF で時計表示や軽いステータス更新をしたい →
DispatcherTimer -
Tick のたびに UI を直接触りたい →
DispatcherTimer -
定期処理の本体が
awaitだらけで、停止や例外も自然に扱いたい →PeriodicTimer -
callback ベースの小さなキックを低コストで入れたい →
System.Threading.Timer -
1〜5ms 級の周期精度や揺れの管理が本体 → この 3 つの前に、前回の記事の待機方法を見る
かなり乱暴に 1 行で言うと、
PeriodicTimerは async のためのタイマーSystem.Threading.Timerは ThreadPool callback のためのタイマーDispatcherTimerは UI のためのタイマー
です。
この覚え方だと、大きく外しにくいです。
8. まとめ
.NET のタイマー選びで本当に大事なのは、名前の違いではなく、この 3 点です。
- どこで動くか
- どういう流れで書きたいか
- 重なりや遅れをどう扱うか
方針としては、これだけで十分戦えます。
- async な定期処理なら
PeriodicTimer - 軽い callback を ThreadPool で回すなら
System.Threading.Timer - WPF の UI 更新なら
DispatcherTimer - 精度が主役なら、別の待機方法を見る
タイマーは、名前が似ているので混ざります。 でも、役割はそんなに似ていません。
PeriodicTimerは async フローを整える道具System.Threading.Timerは callback を定期キックする道具DispatcherTimerは UI スレッドで定期更新する道具
この 3 つを分けて考えるだけで、コードはかなり静かになります。
flowchart TB
accTitle: 3つのタイマーの役割のまとめ
accDescr: PeriodicTimerはasyncフローを整える道具、System.Threading.Timerはcallbackを定期キックする道具、DispatcherTimerはUIスレッドで定期更新する道具という役割の違いをまとめる。
t["タイマーの役割"] --> pt["PeriodicTimer: async"]
t --> st["Timer: callback"]
t --> dt["DispatcherTimer: UI"]
t -.-> rt["精度なら待機設計へ"]
図18: 名前は似ていても役割は似ていない。この3分類で覚えると大きく外さない。
逆にここが混ざると、
- async のはずが
async voidっぽくなる - UI を直接触って落ちる
- callback が重なって状態が濁る
- 周期精度の話まで一緒くたになる
という、わりと普通に面倒なことが起きます。
まずは「どこで動かしたいか」から見る。 それだけで、タイマー選びはだいぶ穏やかになります。
9. 参考資料
- この記事のサンプルコード一式(ライブラリ、デモ、ユニットテスト) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/periodictimer-system-threading-timer-dispatchertimer-guide
- 関連記事: 普通のWindowsでソフトリアルタイムをできるだけ実現するための実践ガイド - まず見るチェックリスト
- 関連記事: C# async/await実務判断表 - Task.RunとConfigureAwait
- 関連記事: WPF/WinFormsのasyncとUIスレッドを一枚で整理
- Timers - .NET
- PeriodicTimer Class
- PeriodicTimer.WaitForNextTickAsync(CancellationToken) Method
- PeriodicTimer.Dispose Method
- PeriodicTimer Constructor
- Timer Class (System.Threading)
- Timer Constructor (System.Threading)
- Background tasks with hosted services in ASP.NET Core
- DispatcherTimer Class (System.Windows.Threading)
- DispatcherTimer Class (Microsoft.UI.Xaml)
- DispatcherTimer Constructor(既定は Background 優先度)
- DispatcherPriority Enum
- Timer Class (System.Windows.Forms)(精度は 55 ミリ秒程度)
- Async/Await - Best Practices in Asynchronous Programming(async void の例外の扱い)
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
.NET Generic HostとBackgroundServiceをデスクトップアプリで使う理由
Windows ツールや常駐アプリで起動、定期処理、終了処理、ログ、設定、DIを整理するために、Generic HostとBackgroundServiceをどう使うかまとめます。
マルチスレッドの実務ベストプラクティス .NET編 ── スレッドを増やす前に決めておくこと
「スレッドを立てたら、たまに落ちる・固まる」を防ぐ設計の定石を.NET/C#向けに整理。スレッドを自分で作らずTaskに乗る、共有可変状態を減らす、ロックの規律、CancellationTokenでの停止設計、UIスレッドの扱いまで解説します。
業務システムのコード設計 ── 商品コード・顧客コードの決め方とチェックディジット
商品コード・顧客コードなど業務システムのコード体系を決める実践ガイド。有意コードと無意味連番の判断表、JAN・Luhn等のチェックディジット算式とC#実装、Excelの0落ち対策、桁あふれと移行まで整理します。
WinForms / WPFアプリのCI/CD実践 ── GitHub Actionsでビルドから署名・配布まで自動化する
WinForms / WPFアプリのCI/CDをGitHub Actionsで組む実務ガイド。windows-latestでのビルド+テストの最小YAML、タグ駆動のバージョン採番、signtoolによる署名の組み込み、MSI/MSIX/ClickOnce/xcopy別のC...
スリープ・休止・Modern Standbyと長時間稼働アプリ ── 「夜中に止まっていた」を設計で防ぐ
長時間動き続けるWindowsアプリが「朝見たら止まっていた」となる原因を、S3スリープ/休止/Modern Standbyの違いから整理します。スリープ中のタイマーやTCP接続の挙動、SetThreadExecutionStateによる抑止まで解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
UI スレッド / タイマーテーマ
WPF / WinForms、UI スレッド、async/await、タイマー設計を整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
定期実行や UI 更新、バックグラウンド処理を含む Windowsアプリ開発 では、タイマー選定がそのまま実装品質に効きます。
技術相談・設計レビュー
PeriodicTimer と DispatcherTimer の責務をどこで分けるか整理したい段階なら、技術相談・設計レビューとして検討できます。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- PeriodicTimerとSystem.Threading.Timerの違いは何ですか?
- いちばん大きな違いは、PeriodicTimerがtickをawaitして待つ型で、System.Threading.Timerがcallback型であることです。PeriodicTimerは「待つ→処理する→また待つ」を1本のasyncメソッドの流れとして書け、CancellationTokenも下流へ渡しやすいです。一方System.Threading.TimerのcallbackはThreadPoolで動き、前回のcallback完了を待たないため、間隔より処理が長いと重なりえます。非同期の定期処理ならPeriodicTimer、軽い同期的なcallbackの定期キックならSystem.Threading.Timerが向いています。
- PeriodicTimerは処理が遅れたとき自動で追いついてくれますか?
- いいえ。前の処理が長引いたからといって、勝手に並列実行して追いついてくれるわけではありません。待っていない間に複数回tickしても、それは1回に畳まれます。そのため、遅れたらスキップするのか、最新だけ見ればよいのか、必ず全回数ぶん処理したいのかは、設計側で決める必要があります。また1つのタイマーに対して同時に複数のWaitForNextTickAsyncを飛ばす前提でもありません。
- System.Threading.Timerにasyncラムダを渡してはいけないのですか?
- 避けたほうがよいです。TimerCallbackはvoidなので、渡したasyncラムダは実質async void的な扱いになります。呼び出し側がawaitできず、完了を待てず、例外管理も難しくなり、callbackの重なりも別途考える必要が出てきます。処理本体がasyncなら、まずPeriodicTimerを検討したほうが読みやすく安全です。
- DispatcherTimerはどんなときに使うべきですか?
- WPFで時計表示や軽いステータス表示など、UIを定期更新したいときに使います。TickがWPFのDispatcher(UIスレッド)上で処理されるため、ハンドラの中でUIをそのまま触れるのが強みです。ただしUIスレッドで動くぶん、Tickに重い処理やブロッキングI/Oを入れると入力や描画まで巻き込んで遅くなります。また指定時刻ぴったりの発火は保証されません。Tickの中身は軽くし、重い仕事は背景へ逃がし、画面を閉じるときはStop()と購読解除で寿命を明示するのが安定します。