更新记录(仅首版,2026年03月12日 发布)
- 首次发布
引用本文(DOI: 10.5281/zenodo.21615446)
本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。
小村 豪(2026)。《PeriodicTimer / System.Threading.Timer / DispatcherTimer 怎么选 - 先理清 .NET 的定期执行》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615446 https://comcomponent.com/zh-CN/blog/2026/03/12/002-periodictimer-system-threading-timer-dispatchertimer-guide/
- DOI(最新版本)
- 10.5281/zenodo.21615446
- DOI(此版本)
- 10.5281/zenodo.21615447
上一篇 尽可能在普通 Windows 上实现软实时的实践指南 - 先看的检查清单 中,整理了避免依赖 Sleep 的周期循环,使用事件驱动或 waitable timer 的话题。
那么在更日常的 .NET 应用程序开发中该怎么做呢。
这里容易迷惑的是 PeriodicTimer、System.Threading.Timer、DispatcherTimer。
名字都是计时器,但,
- 用
await等待 tick 的计时器 - 从 ThreadPool 飞来 callback 的计时器
- 在 UI 线程的
Dispatcher上运作的计时器
性格相当不同。
在实务上容易混在一起的大致是这一带。
- 明明是异步的定期处理,却把
asynclambda 传给System.Threading.Timer - 明明是 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 更自然。
另外,这里处理的是该怎么写应用程序端的定期执行。 周期的准确性本身是主题时,回到上一篇软实时文章的话题。
1. 先讲结论(一句话)
- 想以
await为基础自然地写固定间隔的处理的话,先用PeriodicTimer - 想在 ThreadPool 上定期触发轻量 callback 的话,用
System.Threading.Timer - 想在 WPF 的 UI 线程更新画面的话,用
DispatcherTimer System.Threading.Timer的 callback 可能重叠。把异步处理粗略塞进去容易混乱DispatcherTimer能直接操作 UI 的代价是,放入沉重处理容易连 UI 一起卡住- 在上一篇软实时的语境中,这 3 个不是高精度等待的主角
简言之,最先该看的是以下 3 个。
- 想在哪个线程 / 上下文运行
- 想用
async/await连续写处理本体吗 - 能允许 callback 的重叠吗
光把这 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?"}
F -- "是" --> G["System.Threading.Timer"]
F -- "否" --> H["也考虑其他设计<br/>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 消费者前提。延迟不会自动并发化 |
| 想在 ThreadPool 上执行轻量 heartbeat / 指标发送 / 缓存过期检查 | 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.Timer和DispatcherTimer是 callback / event 型PeriodicTimer是awaittick 等待型
也就是说,
- callback 型是「计时器主动调用过来」
PeriodicTimer是「这边等待下一个 tick」
的区别。
处理本体是 async,
想把「等待 → 处理 → 再等待」当作 1 条流程阅读的话,PeriodicTimer 更自然。
相反地,
- 想接入已有的 callback 基础设计
- 处理本体简短且同步
- 单纯想定期触发一下
这样的场景,System.Threading.Timer 更合适。
PeriodicTimer 虽然方便,但不是万能的。
它不是以对一个计时器同时发出多个 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
- 读文件
- await 其他 async API
这类以 I/O 等待为中心的场景,配合度相当高。
注意点有 2 个。
- 以 1 计时器 1 消费者为前提使用
- 处理时间比周期长时的方针要自己决定
PeriodicTimer 不会因为之前的处理拖长就自动并发执行来追赶。
在这个意义上,它是为了「自然地写固定间隔 async 循环」而设计的计时器。
从测试便利性来看,能用接收 TimeProvider 的构造函数也很朴素好用。
4.2. 在 ThreadPool 上触发轻量 callback 就用 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 以可重入为前提
- 处理时间比间隔长会重叠
所以处理不轻量的情况下,
- 跳过重复启动
- 堆到队列中
- 转向使用
PeriodicTimer
这样设计比较稳妥。
另一个朴素但重要的是保持引用。
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);
}
}
DispatcherTimer 好的地方是 Tick 在 WPF 的 Dispatcher 上处理。
所以可以直接操作 UI。
例如,
- 时钟显示
- 连接状态的轻量显示更新
- Command 的重新评估触发
- 画面上数值的轻量更新
这类场景配合度很好。
但是,这里也有需要留意气氛变化的点。
DispatcherTimer 在 UI 线程运行,
在 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. 直接把 async lambda 传给 System.Threading.Timer
这个相当常见。
_timer = new Timer(async _ => await RefreshAsync(), null,
TimeSpan.Zero, TimeSpan.FromSeconds(5));
外观清爽,但 TimerCallback 是 void。
也就是说这个 async lambda 实质上是 async void 式的处理。
这样的话,
- 调用端无法 await
- 无法等待完成
- 异常管理困难
- callback 的重叠需要另外考虑
是相当泥泞的状态。
处理本体是 async 的话,先考虑 PeriodicTimer 会更易读。
5.2. 在 DispatcherTimer 的 Tick 中放入沉重处理
DispatcherTimer 能直接操作 UI,所以不知不觉就想什么都往里写。
但那里是 UI 线程。
- 长时间的同步处理
- 沉重的 CPU 计算
- 阻塞 I/O
- 可能重复启动的长
await处理
放进去的话,会和 UI 的输入或绘制正面冲突。
把 Tick 的内容变轻, 沉重的工作逃到后台,只把必要的结果回传到 UI 才更稳定。
5.3. 以为 PeriodicTimer 会自动补回延迟
这里也容易误解。
PeriodicTimer 作为干净地写固定间隔 async 循环的工具虽然优秀,
但上次处理拖长时不会自动并发执行去追赶。
没在等待期间发生的 tick 也会被合并为 1 次,
- 延迟就跳过吗
- 只看最新一次就好吗
- 必须处理所有次数吗
需要通过设计来决定。
5.4. 把停止和生命周期管理放到最后
计时器,停止比运行更容易出问题。
容易被漏看的例如以下。
- 把
System.Threading.Timer做成局部变量而不保持引用 - 不停止
System.Threading.Timer就让Dispose()周边变得模糊 - 不对
DispatcherTimer调用Stop()也不退订 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 计时器选择真正重要的,比起名字的差异,是以下几点。
- 在哪里运行
- 想以怎样的流程书写
- 怎么处理重叠或延迟
先按以下方针,就相当能应对了。
- async 的定期处理就用
PeriodicTimer - 在 ThreadPool 上触发轻量 callback 就用
System.Threading.Timer - WPF 的 UI 更新就用
DispatcherTimer - 精度是主角的话就看别的等待方法
计时器因为名字相似所以容易混淆。 但角色并没有那么相似。
PeriodicTimer是整理 async 流程的工具System.Threading.Timer是定期触发 callback 的工具DispatcherTimer是在 UI 线程定期更新的工具
光是把这 3 个分开思考,代码就会变得相当清爽。
相反地这里混在一起,
- 明明是 async 却变成
async void式 - 直接操作 UI 出问题
- callback 重叠状态变混乱
- 连周期精度的话题也一起混杂
这样相当普通但麻烦的事情就会发生。
先从「想在哪里运行」看起。 光这样计时器选择就会相当稳妥。
9. 参考资料
- 相关文章:尽可能在普通 Windows 上实现软实时的实践指南 - 先看的检查清单
- 相关文章:C# 中 async/await 的最佳实践 - 先看的判断表
- 相关文章:用一页整理 WPF / WinForms 的 async/await 和 UI 线程 - await 之后的回归处、Dispatcher、ConfigureAwait、.Result / .Wait() 的卡点
- 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)
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
在桌面应用中使用 .NET Generic Host 与 BackgroundService 的理由
本文整理在 Windows 工具或常驻应用中,如何使用 Generic Host 与 BackgroundService 来梳理启动、定期处理、退出处理、日志、配置与 DI。
多线程实务最佳实践 .NET 篇 ── 在增加线程之前应先确定的事
针对 .NET/C# 整理「立了线程之后,偶尔崩溃・卡死」的防范设计准则。内容涵盖不自行创建线程而改用 Task、减少共享可变状态、锁的纪律、基于 CancellationToken 的停止设计,直至 UI 线程的处理方式。
业务系统的编码设计 ── 商品编码・客户编码的确定方法与校验位
确定商品编码・客户编码等业务系统编码体系的实践指南。整理了有意义编码与无意义流水号的判断表、JAN・Luhn等校验位算法及C#实现、Excel开头零丢失的应对方法,直至位数溢出与迁移。
WinForms / WPF 应用的 CI/CD 实践 ── 用 GitHub Actions 实现从构建到签名、发布的自动化
一份用 GitHub Actions 搭建 WinForms / WPF 应用 CI/CD 的实务指南。整理了在 windows-latest 上构建+测试的最小 YAML、标签驱动的版本编号、集成 signtool 完成签名,以及按 MSI/MSIX/ClickOnce/...
Windows 应用的任务栏托盘常驻与 Toast 通知 —— NotifyIcon 的坑与 AppNotification 的选型
本文整理了将业务 Windows 应用常驻在任务栏托盘(通知区域)并通过 Toast 通知告知用户的实现要点。内容涵盖 NotifyIcon 的正确用法与「关闭后驻留托盘」的设计、资源管理器重启后的重新注册、三种 Toast API(Windows App SDK AppN...
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
UI 线程 & 计时器
整理 WPF / WinForms UI 线程、异步流程、Dispatcher 使用、计时器判断的主题页面。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
支持包含常驻处理、设备联动、运行日志与可维护结构的 Windows 桌面应用程序。
技术咨询 & 设计评审
协助梳理设计方向、架构边界、生命周期责任,以及既有 Windows 资产的处理方式。
常见问题
汇总了咨询这一主题时常见的问题。
- PeriodicTimer、System.Threading.Timer、DispatcherTimer 该怎么选?
- 想以 await 为基础自然地写固定间隔的 async 处理,先用 PeriodicTimer;想在 ThreadPool 上定期触发轻量 callback,用 System.Threading.Timer;想在 WPF 的 UI 线程更新画面,用 DispatcherTimer。判断时先看三件事:想在哪个线程运行、想不想用 async/await 连续写处理本体、能不能允许 callback 重叠。
- 为什么不该把 async lambda 直接传给 System.Threading.Timer?
- 因为 TimerCallback 是 void,传入的 async lambda 实质上变成 async void 式的处理。调用端无法 await、无法等待完成、异常管理困难,callback 的重叠也需要另外考虑。而且 System.Threading.Timer 不会等待上次 callback 完成,处理时间比间隔长就会重叠执行。处理本体是 async 的话,先考虑 PeriodicTimer 会更易读。
- PeriodicTimer 会自动补回延迟的 tick 吗?
- 不会。上次处理拖长时,PeriodicTimer 不会自动并发执行来追赶,没在等待期间发生的多次 tick 也会被合并为 1 次。因此延迟时要跳过、只看最新一次,还是处理所有次数,需要自己以设计来决定。它的前提是 1 个计时器对应 1 个消费者,强项不是周期精度,而是能把「等待、处理、再等待」写成一条自然的 async 流程。
- DispatcherTimer 有什么注意点?
- DispatcherTimer 在 UI 线程运行,可以直接操作 UI,但在 Tick 处理器中放入沉重处理会连输入、绘制、重新布局都拖慢。它也不保证在指定时刻精确触发,会受 Dispatcher 队列中其他任务或优先级影响。实务上要把 Tick 的内容变轻,沉重的 I/O 或 CPU 计算移到后台,画面关闭时记得调用 Stop() 并取消 Tick 订阅,否则会拖住对象的生命周期。