简介
-
在异步编程中,常见的定时任务通常使用
System.Timers.Timer、System.Threading.Timer或者循环中配合Task.Delay。 -
这些方式或需要显式管理回调线程、或需编写复杂的取消逻辑,或容易因累积延迟导致执行不准。
-
PeriodicTimer(.NET 6+引入于System.Threading)提供了一个基于IAsyncDisposable的异步定时器,天然与async/await协作,让按固定间隔执行异步循环更简洁、安全、准时。
PeriodicTimer 解决了这些问题:
-
异步友好:通过
WaitForNextTickAsync方法支持async/await,简化异步任务处理。 -
轻量高效:避免回调复杂性,减少线程切换开销。
-
可取消性:通过
CancellationToken支持优雅取消。 -
高性能:设计用于高吞吐量场景,适合现代
.NET应用(如ASP.NET Core、微服务)。
PeriodicTimer 不直接触发任务,而是提供一种机制,让开发者在循环中等待下一次"滴答"(tick),从而执行自定义逻辑。它特别适合需要定期轮询或执行异步任务的场景。
支持环境
-
目标框架:
-
.NET 6+(内置) -
.NET Core 3.1及以下不支持;需升级到.NET 6或更高。
-
-
命名空间:
csharp
using System.Threading;
NuGet:无需额外安装,随.NET 6+运行时自带。
核心功能
| 功能 | 描述 |
|---|---|
new PeriodicTimer(TimeSpan) |
创建一个以指定间隔触发的异步定时器 |
WaitForNextTickAsync() |
异步等待下一次"滴答"到来;返回 bool,若定时器已被 Dispose 或取消,则返回 false |
DisposeAsync() |
异步释放资源,并使后续 WaitForNextTickAsync 返回 false |
-
精确度:以 启动完成 的时刻为基准,间隔是固定的;不会因单次处理耗时而自动累积延迟。
-
取消支持:可结合
CancellationToken使用WaitForNextTickAsync(ct),支持外部取消。
主要 API 详解
| 成员 | 说明 |
|---|---|
PeriodicTimer(TimeSpan period) |
构造函数,指定两次 tick 之间的间隔 |
ValueTask<bool> WaitForNextTickAsync() |
等待下一次定时;如果定时器活跃则返回 true,否则 false |
ValueTask<bool> WaitForNextTickAsync(CancellationToken) |
支持取消等待 |
ValueTask DisposeAsync() |
异步释放定时器,结束所有挂起的 WaitForNextTickAsync 调用 |
使用示例
简单循环
csharp
async Task RunPeriodicWorkAsync(CancellationToken ct)
{
// 每隔 2 秒执行一次
await using var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));
while (await timer.WaitForNextTickAsync(ct))
{
// 异步执行任务
Console.WriteLine($"执行时间:{DateTime.Now:HH:mm:ss}");
// 可执行异步 IO 或 CPU 任务
await DoWorkAsync(ct);
}
Console.WriteLine("已取消或已完成定时器");
}
使用 CancellationToken 取消
csharp
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // 5秒后取消
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
int count = 0;
try
{
while (await timer.WaitForNextTickAsync(cts.Token))
{
Console.WriteLine($"Tick {++count} at {DateTime.Now:HH:mm:ss}");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Timer cancelled");
}
}
}
累积延迟隔离
csharp
async Task RunWithPreciseInterval(CancellationToken ct)
{
await using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(ct))
{
var start = DateTime.UtcNow;
await DoWorkAsync(ct); // 假设耗时 1s
// 即使 DoWorkAsync 耗时,下一次 tick 也会在上一次开始 +5s 时触发
var elapsed = DateTime.UtcNow - start;
Console.WriteLine($"周期耗时:{elapsed.TotalSeconds:F2}s");
}
}
配合 IHostedService (ASP.NET Core 后台服务)
csharp
public class TimedBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
// 每分钟执行一次
await DoMaintenanceAsync(stoppingToken);
}
}
}
错误处理
处理任务中的异常:
csharp
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
int count = 0;
while (await timer.WaitForNextTickAsync())
{
try
{
if (++count % 3 == 0)
throw new Exception("Simulated error");
Console.WriteLine($"Tick {count} at {DateTime.Now:HH:mm:ss}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
}
在循环中捕获异常,确保定时器继续运行。
心跳检测
csharp
async Task StartHeartbeatAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
while (await timer.WaitForNextTickAsync(ct))
{
var isHealthy = await CheckSystemHealthAsync();
if (!isHealthy)
{
await AlertAdminAsync("系统异常!");
}
}
}
实时数据轮询
csharp
async Task PollStockPricesAsync(string symbol)
{
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(250));
decimal? lastPrice = null;
while (await timer.WaitForNextTickAsync())
{
var currentPrice = await GetStockPriceAsync(symbol);
if (currentPrice != lastPrice)
{
DisplayPriceUpdate(symbol, currentPrice);
lastPrice = currentPrice;
}
}
}
批量数据处理
csharp
async Task ProcessQueueBatchAsync()
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync())
{
var messages = GetQueuedMessages(maxCount: 100);
if (messages.Count > 0)
{
await ProcessBatchAsync(messages);
}
else
{
// 无数据时降低频率
timer.Period = TimeSpan.FromSeconds(30);
}
}
}
与传统方案对比实践
替代 Task.Delay 循环
csharp
// 传统方式(问题:时间漂移积累)
async Task OldApproach()
{
while (true)
{
await DoWorkAsync();
await Task.Delay(1000); // 实际间隔 = 工作耗时 + 1秒
}
}
// PeriodicTimer 方案(固定间隔)
async Task NewApproach()
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync())
{
await DoWorkAsync(); // 间隔严格1秒(不考虑工作耗时)
}
}
替代 System.Threading.Timer
csharp
// 传统回调方式
var timer = new Timer(_ =>
{
// ❌ 同步上下文问题
DoWork().Wait(); // 死锁风险
}, null, 0, 1000);
// PeriodicTimer 安全替代
async Task RunAsync()
{
using var pt = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await pt.WaitForNextTickAsync())
{
await DoWorkAsync(); // ✅ 安全异步
}
}
高阶用法
动态间隔调整
csharp
TimeSpan interval = TimeSpan.FromSeconds(1);
using var timer = new PeriodicTimer(interval);
while (await timer.WaitForNextTickAsync())
{
try
{
// 执行任务
await ProcessData();
// 成功时加速
interval = TimeSpan.FromMilliseconds(500);
}
catch
{
// 出错时减速
interval = TimeSpan.FromSeconds(5);
}
// 动态更改间隔
timer.Period = interval;
}
实现原理
-
定时机制:
PeaiodicTimer内部使用高精度计时器(基于操作系统内核),以固定间隔触发滴答。 -
异步等待:
WaitForNextTickAsync返回ValueTask<bool>,使用异步状态机避免阻塞线程。 -
取消支持:通过
CancellationToken与内部计时器集成,允许优雅取消。 -
资源管理:实现
IDisposable,释放时停止计时器并清理资源。 -
性能优化:
-
使用
ValueTask减少分配(相比Task)。 -
避免回调模型,降低线程池竞争。
-
性能与对比
| 特性 | PeriodicTimer |
Task.Delay + 循环 |
System.Timers.Timer / Threading.Timer |
|---|---|---|---|
| 精确度 | 高(基于上次启动时刻) | 随循环体耗时累积误差 | 基于系统回调,可能发生重入 |
| 取消与资源释放 | 原生支持 CancellationToken 与 DisposeAsync |
需手动管理 CancellationToken |
需要 Stop/Dispose |
| 异步友好 | 与 async/await 无缝结合 |
需额外包装 | 回调式,不易组合 async |
| 重叠执行保护 | 不会同时启动多个 Tick | 需手动防止重入 | 可选 AutoReset/SynchronizationObject |
| 依赖与复杂度 | 最少,仅依赖 BCL | 最少 | 依赖事件模型 |
使用场景
理想应用场景
-
心跳检测:每30秒发送心跳包
-
数据轮询:定期检查
API/数据库更新 -
资源清理:每5分钟清理临时文件
-
实时看板:每秒刷新UI数据
-
批处理系统:定时触发数据处理流水线
不适用场景
-
高精度定时(<10ms精度)
-
硬件级中断处理
-
跨进程同步
-
需要精确回调时间的场景
-
.NET Framework项目
使用注意事项
准确性假设
PeriodicTimer保证每次从上一轮开始时刻计算下一次触发时间;如果处理耗时超过周期,下一次会立即(或接近立即)返回,不会排队多次。
异常与终止
-
若循环体内抛出未捕获异常,则外层
while会终止,DisposeAsync需要在finally中确保调用。 -
推荐将
WaitForNextTickAsync放在try块外,仅捕获循环体内部异常。
取消模式
- 使用
WaitForNextTickAsync(ct),当传入的CancellationToken被触发后,该方法会抛出OperationCanceledException或返回false(取决于时机),循环优雅退出。
资源释放
- 必须调用
DisposeAsync,否则底层可能保留未完成的定时操作及注册的回调,导致内存或计时器句柄泄漏。
总结
PeriodicTimer 是 .NET 6+ 提供的一个轻量级、异步友好的定时器,专为基于 async/await 的场景设计。它通过固定周期、无累积误差、天然取消支持及简单的资源释放方式,让定时异步循环的编写更加直观和可靠。在需要周期性执行异步任务、并发安全且易于取消和清理的场景下,强烈推荐使用 PeriodicTimer。