簡介
- 在異步編程中,常見的定時任務通常使用
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或更高。
- 命名空間:
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 調用 |
使用示例
簡單循環
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 取消
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");
}
}
}
累積延遲隔離
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 後台服務)
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);
}
}
}
錯誤處理
處理任務中的異常:
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}");
}
}
}
}
在循環中捕獲異常,確保定時器繼續運行。
心跳檢測
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("系統異常!");
}
}
}
實時數據輪詢
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;
}
}
}
批量數據處理
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 循環
// 傳統方式(問題:時間漂移積累)
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
// 傳統回調方式
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(); // ✅ 安全異步
}
}
高階用法
動態間隔調整
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。