You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 05:20:32