C# Academy · 课时

周期性任务与计时器

使用 PeriodicTimer 和 System.Threading.Timer 安排重复工作,并优雅地管理取消操作。

第 3 / 4 课12 个步骤

周期性任务与计时器 是 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. IHostedService 与 BackgroundService
  2. Worker Service 项目
  3. 周期性任务与计时器
  4. Quartz.NET 计划任务
← 返回 C# Academy