周期性任务与计时器
使用 PeriodicTimer 和 System.Threading.Timer 安排重复工作,并优雅地管理取消操作。
周期性任务与计时器 是 CoddyKit 上的免费 C# Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。
安排周期性工作
许多后台服务需要按固定计划运行——每 5 分钟、每小时或每天运行一次。.NET 提供了多种机制:PeriodicTimer(现代方案)、System.Threading.Timer(经典方案)和 Quartz.NET(基于 cron)。
PeriodicTimer:现代选择
PeriodicTimer(在 .NET 6 中引入)专为在 async 方法中使用而设计。它无需回调即可严格按计划触发,并能与取消机制良好集成。
public class MetricsCollectorService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(ct))
{
// Runs every 60 seconds
await CollectMetricsAsync(ct);
}
// Loop exits cleanly when ct is cancelled
}
}PeriodicTimer 与 Task.Delay 循环
相比 Task.Delay 循环,PeriodicTimer 更为合适:它从开始时间起按固定间隔触发,而不是按照“开始时间 + 处理时间”计算,从而避免多次迭代后出现时间漂移。
// Task.Delay loop — drifts! (interval = delay + work time)
while (!ct.IsCancellationRequested)
{
await DoWorkAsync(ct); // takes 2 seconds
await Task.Delay(5000, ct); // actual period: 7 seconds
}
// PeriodicTimer — no drift
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(ct))
{
await DoWorkAsync(ct); // period is still 5 seconds from start
}System.Threading.Timer
System.Threading.Timer 是一种经典的线程池计时器,使用回调运行。它同时支持周期性模式和一次性模式。请注意:回调在线程池线程上运行,不支持 async。
public class LegacyTimerService : BackgroundService, IDisposable
{
private Timer? _timer;
protected override Task ExecuteAsync(CancellationToken ct)
{
// Fire immediately then every 10 seconds
_timer = new Timer(DoWork, null,
dueTime: TimeSpan.Zero,
period: TimeSpan.FromSeconds(10));
return Task.CompletedTask;
}
private void DoWork(object? state)
{
// Runs on thread pool — avoid async void here
Console.WriteLine($"Timer fired: {DateTime.UtcNow}");
}
public override void Dispose() { _timer?.Dispose(); base.Dispose(); }
}一次性延迟执行
对于一次性延迟执行,可以使用只触发一次的 PeriodicTimer,或者将 CancellationTokenSource.CancelAfter 与 Task.Delay 结合使用。
// Run once after a delay
public async Task ScheduleOnceAsync(TimeSpan delay, Func<Task> action, CancellationToken ct)
{
await Task.Delay(delay, ct);
await action();
}
// Or with CancellationToken:
var cts = new CancellationTokenSource(TimeSpan.FromHours(1));
try
{
await Task.Delay(Timeout.Infinite, cts.Token);
}
catch (OperationCanceledException)
{
await RunScheduledJobAsync();
}带错误处理的 PeriodicTimer
请始终使用 try/catch 包装每次触发时执行的工作,以免单次故障阻止计时器在下一个周期继续触发。
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(ct))
{
try
{
await SyncInventoryAsync(ct);
}
catch (Exception ex)
{
// Log but don't crash — next tick will retry
_logger.LogError(ex, "Inventory sync failed");
}
}
}与时钟边界对齐
如果要在特定时间运行(例如每小时整点运行),请在启动计时器之前,计算出距离下一个时间边界的初始延迟。
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Wait until the next full hour
var now = DateTime.UtcNow;
var nextHour = now.Date.AddHours(now.Hour + 1);
var initialDelay = nextHour - now;
await Task.Delay(initialDelay, ct);
// Then tick every hour exactly
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
await RunJobAsync(ct); // run at the first tick
while (await timer.WaitForNextTickAsync(ct))
await RunJobAsync(ct);
}防止触发并发执行
如果工作耗时可能超过一个计时器周期,请使用 SemaphoreSlim 防止并发执行,或者在工作仍在运行时跳过本次触发。
private readonly SemaphoreSlim _lock = new(1, 1);
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(ct))
{
// Skip if previous tick is still running
if (!await _lock.WaitAsync(0, ct))
{
_logger.LogWarning("Skipping tick — previous still running");
continue;
}
try { await DoWorkAsync(ct); }
finally { _lock.Release(); }
}
}分布式服务中的随机抖动
当服务的多个实例同时启动时,在初始延迟中加入随机抖动,可以避免共享资源遭遇惊群问题。
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Add jitter: 0-5 seconds random delay on startup
var jitter = TimeSpan.FromSeconds(Random.Shared.NextDouble() * 5);
await Task.Delay(jitter, ct);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(ct))
await RunJobAsync(ct);
}实际案例:缓存刷新服务
一个每 10 分钟刷新分布式缓存的周期性服务,并包含随机抖动和错误恢复机制。
public class CacheRefreshService : BackgroundService
{
private readonly IServiceScopeFactory _factory;
private readonly ILogger<CacheRefreshService> _log;
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Spread startups across instances
await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(0, 30)), ct);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(10));
while (await timer.WaitForNextTickAsync(ct))
{
try
{
using var scope = _factory.CreateScope();
var cache = scope.ServiceProvider.GetRequiredService<ICacheService>();
await cache.RefreshAllAsync(ct);
_log.LogInformation("Cache refreshed at {Time}", DateTime.UtcNow);
}
catch (Exception ex) { _log.LogError(ex, "Cache refresh failed"); }
}
}
}快速检查
对于周期性任务,为什么更推荐使用 PeriodicTimer,而不是 Task.Delay 循环?
回顾:周期性任务与计时器
关键要点:
PeriodicTimer:支持 async、可响应取消、不会发生时间漂移——现代选择System.Threading.Timer:基于回调,适合与旧代码集成- 使用 try/catch 包装每次触发时执行的工作,避免错误停止计时器
- 为分布式实例添加随机抖动,避免惊群问题
- 使用
SemaphoreSlim在上一次触发仍在运行时跳过本次触发 - 计算初始延迟,使触发时间与时钟边界对齐
用 AI 导师学习 C# — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 93
- 课程
- 346
常见问题解答
「周期性任务与计时器」课时是免费的吗?
是的 — 「周期性任务与计时器」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。
「周期性任务与计时器」这节课中我会学到什么?
使用 PeriodicTimer 和 System.Threading.Timer 安排重复工作,并优雅地管理取消操作。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C# Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「周期性任务与计时器」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C# Academy 课中编写并运行代码吗?
能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。