.NETタイマー3種の使い分け - PeriodicTimer/Timer/DispatcherTimer

· 更新日: · · C#, .NET, WPF, タイマー, 設計

更新履歴(4件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
サンプルリポジトリの実在するテストからの抜粋を追加しました(コールバックの重なりと、放置後のtickの畳み込み)。`DispatcherPriority.Background`が既定であることの説明、WinFormsのタイマーは精度が55ミリ秒程度であること、`async void`相当の例外がスレッドプール上ではプロセスごと落とすことを追加しました。
本文中の関連記事へのリンクの文言が、リンク先の現在のタイトルと食い違っていたのを、実際のタイトルに揃えました。本文の内容は変えていません。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589623)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「.NETタイマー3種の使い分け - PeriodicTimer/Timer/DispatcherTimer」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589623 https://staging.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 アプリ開発ではどうするのか。 ここで迷いやすいのが、PeriodicTimerSystem.Threading.TimerDispatcherTimer です。

名前はどれもタイマーですが、

  • await でティックを待つタイマー
  • ThreadPool で callback が飛んでくるタイマー
  • UI スレッドの Dispatcher 上で動くタイマー

というように、性格がかなり違います。

実務で混ざりやすいのは、だいたいこのへんです。

  • 非同期の定期処理なのに System.Threading.Timerasync ラムダを渡してしまう
  • 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 の方も対象に含めています。DispatcherTimerSystem.Windows.Forms.Timer に読み替えれば、4.3 と 5.2 の注意はそのまま当てはまります。そのうえで、次の 2 点だけ違います。

  • System.Windows.Forms.Timer はメッセージループ経由で Tick が起きるシングルスレッドのタイマーで、DispatcherTimer のような優先度(DispatcherPriority)の指定はありません
  • Microsoft のドキュメントで 精度は 55 ミリ秒程度が限界 とされています。細かい周期には向かないので、その場合は UI 用タイマー以外を検討します

なお、ここで扱うのは アプリ側の定期実行をどう書くか です。 周期の正確さそのものが主題 のときは、前回のソフトリアルタイム記事の話に戻ります。

また、この記事に登場するコードは、ビルド・実行できるサンプル一式(PeriodicTimer / System.Threading.Timer のライブラリとコンソールデモ、tick の畳み込みや callback の重なりを検証するユニットテスト)として GitHub で公開しています。

periodictimer-system-threading-timer-dispatchertimer-guide - komurasoft-blog-samples (GitHub)

目次

  1. まず結論(ひとことで)
  2. まず一枚で整理
    • 2.1. 全体像
    • 2.2. まずの判断表
  3. まず区別したいこと
    • 3.1. callback 型か、tick を待つ型か
    • 3.2. ThreadPool で動くのか、UI スレッドで動くのか
    • 3.3. 周期処理と精度保証は別の話
  4. 典型パターン
    • 4.1. async な定期処理なら、PeriodicTimer
    • 4.2. 軽い callback を ThreadPool で回すなら、System.Threading.Timer
    • 4.3. WPF の UI 更新なら、DispatcherTimer
    • 4.4. ソフトリアルタイム寄りの周期処理なら、別の道具を見る
  5. よくあるアンチパターン
  6. レビュー時のチェックリスト
  7. ざっくり使い分け
  8. まとめ
  9. 参考資料

この記事の知識マップ

この記事は.NETの3種類のタイマーを、実行される場所と処理の書き方で整理します。PeriodicTimerはCancellationTokenを使いawaitでtickを待つ型のタイマーで、worker/BackgroundServiceでの非同期の定期処理に向きますが、高精度な周期実行の道具ではありません。System.Threading.TimerはThreadPool上でcallbackが呼ばれる軽量なタイマーで、前回のcallback完了を待たないため重なりが起こり得るうえ、voidであるTimerCallbackにasyncラムダを渡すとasync void相当の扱いになり、ThreadPool上の未処理例外がプロセスを落としかねません。DispatcherTimerはWPFのDispatcher上でTickが処理されるためUIを直接更新できますが、重い処理はUIスレッドの詰まりを招き、Stop()や購読解除を怠るとオブジェクトの寿命を引っ張ることがあります。WinForms用のTimerも精度が55ミリ秒程度に限られる点は共通です。

PeriodicTimer・System.Threading.Timer・DispatcherTimerの使い分けの知識マップPeriodicTimerがCancellationTokenを使いBackgroundServiceでの非同期の定期処理に向くこと、System.Threading.TimerがThreadPool上でcallbackの重なりやasync void相当の落とし穴を持つこと、DispatcherTimerがWPFのDispatcher上でUIスレッドの詰まりや購読解除漏れのリスクと引き換えにUI更新を担うこと、いずれも高精度な周期実行の要件には向かないことを示す図利用する推奨される対応推奨される対応用いるのは非推奨利用する原因になり得る原因になり得る用いるのは非推奨用いるのは非推奨原因になり得る利用する原因になり得る用いるのは非推奨原因になり得る用いるのは非推奨推奨される対応前提とする利用するPeriodicTimerSystem.Threading.TimerDispatcherTimerCancellationToken(.NET)BackgroundService非同期の定期処理高精度な周期実行の要件.NETスレッドプールタイマーcallbackの重なりasync void相当のコールバックThreadPool上の未処理例外によるプロセス終了DispatcherPriorityUIスレッドの詰まりイベント購読解除漏れSystem.Windows.Forms.Timer再入防止ガード(Interlocked.Exchange等)Dispatcher(WPF)

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全18件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

1. まず結論(ひとことで)

  • await ベースで一定間隔の処理を自然に書きたいなら、まず PeriodicTimer
  • ThreadPool 上で軽い callback を定期的に起動したいなら、System.Threading.Timer
  • WPF の UI スレッドで画面更新したいなら、DispatcherTimer
  • System.Threading.Timer は callback が重なりうる。非同期処理を雑に突っ込むと荒れやすい
  • DispatcherTimer は UI を直接触れる代わりに、重い処理を入れると UI ごと止めやすい
  • 前回のソフトリアルタイムの文脈では、この 3 つは高精度待機の主役ではない

要するに、最初に見るべきなのは次の 3 つです。

  1. どのスレッド / コンテキストで動かしたいか
  2. 処理本体を async / await で直列に書きたいか
  3. callback の重なりを許せるか

この 3 つを分けるだけで、かなり迷いにくくなります。

2. まず一枚で整理

2.1. 全体像

はいいいえはいいいえはいいいえ一定間隔で何かしたいUIスレッドで動かしたい?DispatcherTimer処理本体をasync / await で素直に書きたい?PeriodicTimerThreadPool で軽い callback を回したい?System.Threading.Timer別の設計も検討Channel / BackgroundService / event / waitable timer

実務では、だいたいこの分岐で十分です。迷ったときにいちばん外しにくいのは、 非同期処理なら 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 名称より「どこで走るか」を見ていないことのほうが多いです。

3. まず区別したいこと

3.1. callback 型か、tick を待つ型か

ここを分けると、一気に見通しがよくなります。

  • System.Threading.TimerDispatcherTimer は callback / event 型
  • PeriodicTimer は tick を await して待つ型

つまり、

  • callback 型は「タイマー側が呼んでくる」
  • PeriodicTimer は「こちらが次の tick を待つ」

という違いです。

処理本体が async で、 「待つ → 処理する → また待つ」を 1 本の流れとして読みたいなら、PeriodicTimer のほうが自然です。

逆に、

  • 既存の callback ベース設計に載せたい
  • 処理本体が短くて同期的
  • 単純に定期キックしたい

という場面では、System.Threading.Timer が合います。

PeriodicTimer は便利ですが、万能ではありません。 1 つのタイマーに対して同時に複数の WaitForNextTickAsync を飛ばす前提ではなく、 待っていない間に複数回 tick しても、それは 1 回に畳まれます。

ここを「勝手に追いついてくれる」と誤解しないのが大事です。

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 を安全に触れる」ことが強みですが、 それは同時に「重い処理を入れると入力や再描画も巻き込む」という意味でもあります。

3.3. 周期処理と精度保証は別の話

ここは前回の記事とのつながりとして大事です。

一定間隔で何かする、という言い方は同じでも、

  • アプリの都合として、数秒おきに定期処理したい
  • 1ms〜数ms 級で、できるだけ deadline に近づけたい

は、別の問題です。

System.Threading.Timer は軽量で扱いやすいタイマーですが、 精度のための専用道具ではありません。 DispatcherTimer も、Dispatcher キューの都合や優先度の影響を受けます。

PeriodicTimer も名前だけ見ると「周期がきっちりしていそう」に見えますが、 実務での強みは precision というより async フローの書きやすさ です。

なので、

  • アプリの定期実行 を書きたいのか
  • 待機精度 を詰めたいのか

は、最初に分けたほうが安全です。

この 2 つが混ざると、タイマー選びの議論がだんだん変な方向へ行きます。

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 タイマー 1 コンシューマー前提で使う
  2. 処理時間が周期より長いときの方針を、自分で決める

PeriodicTimer は、前の処理が長引いたからといって、自動で並列化して追いついてくれるわけではありません。 その意味では、「一定間隔の async ループを自然に書く」ためのタイマーです。

テストしやすさまで見るなら、TimeProvider を受けるコンストラクターを使えるのも地味に便利です。

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 に寄せる

のように設計したほうが穏やかです。

もう 1 つ地味に大事なのは、参照を保持すること です。 System.Threading.Timer は動作中でも、参照がなくなると GC の対象になります。 また、Dispose() を呼んだ直後でも、すでにキューされた callback が後から走ることがあります。

つまり System.Threading.Timer は、

  • 軽量
  • 速い
  • シンプル

ですが、その代わりに 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 の中身を軽く保つ前提がより強くなります。

DispatcherTimer のよいところは、Tick が WPF の Dispatcher 上で処理されるため、UI をそのまま触れることです。

これはたとえば、

  • 時計表示
  • 接続状態の軽い表示更新
  • Command の再評価きっかけ
  • 画面に出ている数値の軽い更新

のような場面と相性がよいです。

ただし、ここでも空気が変わる点があります。

DispatcherTimerUI スレッドで動く ので、 Tick ハンドラで重い処理をすると、そのまま入力・描画・再配置まで巻き込んで遅くなります。

また、DispatcherTimer は「指定時刻ぴったり」を保証する道具ではありません。 Dispatcher キュー上の他の仕事や優先度の影響を受けます。

なので実務では、

  • Tick の中身は軽くする
  • 重い I/O や CPU は別へ逃がす
  • 閉じるときは Stop() と購読解除をして寿命を明示する

くらいまで意識しておくと安定します。

4.4. ソフトリアルタイム寄りの周期処理なら、別の道具を見る

ここが前回の記事との接続点です。

前回のソフトリアルタイム記事で扱ったのは、 「何秒おきにだいたい動けばよい」ではなく、 周期の揺れや deadline miss をどう減らすか という話でした。

その文脈では、

  • Sleep 任せの相対待機にしない
  • イベント駆動や waitable timer を使う
  • fast path と slow path を分ける
  • 遅れを計測する

が主題になります。

なので、

  • 普段のアプリの async な定期処理 → PeriodicTimer
  • ThreadPool callback → System.Threading.Timer
  • UI 更新 → DispatcherTimer
  • 周期精度そのものが主役 → 前回の記事の世界

というふうに、最初から問題を分けてしまうのがきれいです。

「1ms ごとにできるだけきっちり回したい。どの .NET タイマーがよいか」という問いは、 半分くらいはもうタイマー選びではなく、待機方法と設計の問題です。

5. よくあるアンチパターン

5.1. System.Threading.Timerasync ラムダをそのまま渡す

これはかなりやりがちです。

_timer = new Timer(async _ => await RefreshAsync(), null,
    TimeSpan.Zero, TimeSpan.FromSeconds(5));

見た目はすっきりしていますが、TimerCallbackvoid です。 つまりこの async ラムダは、実質 async void 的な扱いになります。

すると、

  • 呼び出し側が await できない
  • 完了を待てない
  • 例外管理が難しい
  • callback の重なりも別途考える必要がある

という、扱いにくい状態になります。

例外管理が難しい理由は、もう少しはっきり書いておく価値があります。async Task なら例外は Task に載るので、呼び出し側が await した時点で受け取れます。async void(相当)にはその Task が無いので、投げられた例外は そのメソッドが開始したときに有効だった SynchronizationContext へ直接投げ直されますSystem.Threading.Timer の callback は ThreadPool 上で動き、そこに SynchronizationContext はありません。結果として、例外は ThreadPool のスレッド上の未処理例外になり、既定ではプロセスごと落ちます。callback の中を try / catch で自前に包まない限り、外側では受け止められません。

処理本体が 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 回に畳まれることもあるので、

  • 遅れたらスキップするのか
  • 最新だけ見ればよいのか
  • 必ず全回数ぶん処理したいのか

は、設計で決める必要があります。

5.4. 停止と寿命管理を後回しにする

タイマーは、動かすより止めるほうが事故ります。

見落としやすいのは、このあたりです。

  • System.Threading.Timer をローカル変数のまま作って参照を保持しない
  • System.Threading.Timer を止めずに Dispose() まわりを曖昧にする
  • DispatcherTimerStop() せず、Tick 購読も外さない
  • 画面を閉じたあとも、タイマーがオブジェクト寿命を引っ張る

特に DispatcherTimer は、メソッドがバインドされているオブジェクトを生かし続けることがあります。 「なんかこの Window、閉じたはずなのに残ってるな」という妙な感じが出たら、ここを疑いたくなります。

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 点です。

  1. どこで動くか
  2. どういう流れで書きたいか
  3. 重なりや遅れをどう扱うか

方針としては、これだけで十分戦えます。

  1. async な定期処理なら PeriodicTimer
  2. 軽い callback を ThreadPool で回すなら System.Threading.Timer
  3. WPF の UI 更新なら DispatcherTimer
  4. 精度が主役なら、別の待機方法を見る

タイマーは、名前が似ているので混ざります。 でも、役割はそんなに似ていません。

  • PeriodicTimer は async フローを整える道具
  • System.Threading.Timer は callback を定期キックする道具
  • DispatcherTimer は UI スレッドで定期更新する道具

この 3 つを分けて考えるだけで、コードはかなり静かになります。

逆にここが混ざると、

  • async のはずが async void っぽくなる
  • UI を直接触って落ちる
  • callback が重なって状態が濁る
  • 周期精度の話まで一緒くたになる

という、わりと普通に面倒なことが起きます。

まずは「どこで動かしたいか」から見る。 それだけで、タイマー選びはだいぶ穏やかになります。

9. 参考資料

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

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

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

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

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()と購読解除で寿命を明示するのが安定します。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る