如何让Periodic Timer每秒强制触发且不阻塞长时任务
解决PeriodicTimer被长任务阻塞的问题
你的问题核心在于await job.ProcessAsync()会阻塞定时器的循环逻辑——必须等3秒的长任务执行完毕,才会进入下一轮WaitForNextTickAsync的等待,导致定时器无法按1秒的间隔触发。
要实现定时器每秒强制触发+长任务后台独立执行,需要做以下修改:
修改后的代码
1. 调整PeriodicHostedService逻辑
public class PeriodicHostedService : BackgroundService { private readonly TimeSpan period = TimeSpan.FromSeconds(1); private readonly ILogger<PeriodicHostedService> logger; private readonly IServiceScopeFactory factory; private int executionCount = 0; public PeriodicHostedService(ILogger<PeriodicHostedService> logger, IServiceScopeFactory factory) { this.logger = logger; this.factory = factory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using PeriodicTimer timer = new(period); while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken)) { executionCount++; logger.LogInformation($"Triggered PeriodicHostedService - Count: {executionCount}"); // 启动后台任务,不阻塞定时器循环 _ = Task.Run(async () => { try { // 每次任务创建独立的服务作用域,避免作用域提前释放 using var scope = factory.CreateScope(); ITimerJob job = scope.ServiceProvider.GetRequiredService<ITimerJob>(); await job.ProcessAsync(stoppingToken); } catch (Exception ex) { logger.LogError(ex, $"Failed to execute background job for count {executionCount}"); } }, stoppingToken); } } }
2. 更新TimerJob接口和实现(增加取消令牌支持)
public interface ITimerJob { Task ProcessAsync(CancellationToken cancellationToken); } internal class TimerJob : ITimerJob { private int runningID; public async Task ProcessAsync(CancellationToken cancellationToken) { runningID++; Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} > Current Running ID : {runningID}"); await LongTimeJob(cancellationToken); } private async Task LongTimeJob(CancellationToken cancellationToken) { Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} > Step1 Async Job Start ID : {runningID}"); // 传递取消令牌,服务停止时可终止延迟任务 await Task.Delay(3000, cancellationToken).ConfigureAwait(false); Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} > Step1 Async Job End ID : {runningID}"); } }
关键修改点说明
- 取消全局服务作用域:原来的scope在循环外创建,后台任务未完成时scope可能被释放,导致服务实例回收出错。现在每次触发任务都创建独立scope,符合DI生命周期规则。
- 后台任务异步启动:用
Task.Run包裹任务逻辑且不await,让定时器循环立即进入下一轮等待,保证1秒触发间隔不受长任务影响。 - 传递取消令牌:将服务停止的
stoppingToken传给后台任务,确保服务关闭时能及时终止正在运行的长任务,避免资源泄漏。 - 独立异常处理:后台任务的异常无法被外层循环捕获,必须在Task.Run内部处理,防止未捕获异常导致应用崩溃。
内容的提问来源于stack exchange,提问作者Carrie Kaski
相关产品推荐
相关产品推荐

