【问题标题】:Run async hosted service every 5 minutes in ASP.NET Core在 ASP.NET Core 中每 5 分钟运行一次异步托管服务
【发布时间】:2022-11-06 03:23:21
【问题描述】:

用于后台服务的 ASP.NET Core docs 显示了许多实现示例。

有一个在timer 上启动服务的示例,尽管它是同步的。还有另一个异步示例,用于使用scoped dependency 启动服务。

我需要两者都做:每 5 分钟启动一次服务,并且它具有范围依赖性。没有例子。

我结合了这两个示例,但我不确定将Timer 与异步TimerCallback 一起使用的安全方法。

例如

public class MyScheduler : IHostedService
{
  private Timer? _timer;
  private readonly IServiceScopeFactory _serviceScopeFactory;

  public MyScheduler(IServiceScopeFactory serviceScopeFactory) => _serviceScopeFactory = serviceScopeFactory;

  public void Dispose() => _timer?.Dispose();

  public Task StartAsync(CancellationToken cancellationToken)
  {
    _timer = new Timer((object? state) => {
      using var scope = _serviceScopeFactory.CreateScope();
      var myService = scope.ServiceProvider.GetRequiredService<IMyService>();
      await myService.Execute(cancellationToken);            // <------ problem
    }), null, TimeSpan.Zero, TimeSpan.FromSeconds(5));

    return Task.CompletedTask;
  }

  public Task StopAsync(CancellationToken cancellationToken) {
    _timer?.Change(Timeout.Infinite, 0);
    return Task.CompletedTask;
  }

}

计时器接受同步回调,所以问题出在await。调用异步服务的安全方法是什么?

【问题讨论】:

  • myService.Execute 是否有可能需要 5 分钟以上才能完成?在这种情况下,你希望发生什么?您对方法的重叠执行感到满意吗?
  • 您可能会发现这很有用:Run async method regularly with specified interval。从长远来看,在事件处理程序中添加临界区可能会导致ThreadPool 饱和,因为在计时器计时时可能会阻塞越来越多的线程。
  • @lonix 是的,Artur 的answer 防止重叠,因为它不涉及基于事件的Timer,它调用ThreadPool 上的事件处理程序。
  • @lonix“选项2”也可以防止重叠。 PeriodicTimerawaitable timer,而不是基于事件的计时器。
  • @TheodorZoulias 您的 cmets 与下面的答案一样有帮助,谢谢!

标签: c# asp.net-core async-await asp.net-core-5.0 asp.net-core-hosted-services


【解决方案1】:

使用BackgroundService 而不是IHostedService

public class MyScheduler : BackgroundService
{
    private readonly IServiceScopeFactory _serviceScopeFactory;
    public MyScheduler(IServiceScopeFactory serviceScopeFactory) => _serviceScopeFactory = serviceScopeFactory;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Option 1
        while (!stoppingToken.IsCancellationRequested)
        {
            // do async work
            using (var scope = _serviceScopeFactory.CreateScope())
            {
              var myService = scope.ServiceProvider.GetRequiredService<IMyService>();
              await myService.Execute(stoppingToken);
            }
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }

        // Option 2 (.NET 6)
        var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            // do async work
            // ...as above
        }
    }
}

【讨论】:

  • 您的问题上有asp.net-core-5 标签,但请注意.NET 5.0 达到end of support on 08/05/22,因此您最好升级到6.0 并使用PeriodicTimer
  • 我喜欢这种方法。这比我建议的要简单得多。
  • 谢谢。要使用上面显示的范围依赖项,是否会以与您的解决方案相同的方式完成?即在while循环内创建一个范围并在每次滴答时从那里解析服务是“安全的”
  • 是的,同样的方式。
  • 我喜欢“选项 1”而不是我上面的代码的一个原因 - 你不能得到多个重叠的执行。它一次只允许一个。
【解决方案2】:

使用异步处理程序创建一个事件并在每个时间间隔引发它。可以等待事件处理程序

public class MyScheduler : IHostedService {
    private Timer? _timer;
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public MyScheduler(IServiceScopeFactory serviceScopeFactory) => _serviceScopeFactory = serviceScopeFactory;

    public void Dispose() => _timer?.Dispose();
    
    public Task StopAsync(CancellationToken cancellationToken) {
        _timer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }
    
    public Task StartAsync(CancellationToken cancellationToken) {
        performAction += onPerformAction; //subscribe to event
        ScopedServiceArgs args = new ScopedServiceArgs {
            ServiceScopeFactory = _serviceScopeFactory,
            CancellationToken = cancellationToken
        };
        _timer = new Timer((object? state) =>  performAction(this, args), //<-- raise event
            null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
        return Task.CompletedTask;
    }
  
    private event EventHandler<ScopedServiceArgs> performAction = delegate { };
    
    private async void onPerformAction(object sender, CancellationArgs args) {
        using IServiceScope scope = args.ServiceScopeFactory.CreateScope();
        IMyService myService = scope.ServiceProvider.GetRequiredService<IMyService>();
        await myService.Execute(args.CancellationToken);
    }
    
    class ScopedServiceArgs : EventArgs {
        public IServiceScopeFactory ServiceScopeFactory {get; set;}
        public CancellationToken CancellationToken {get; set;}
    }

}

【讨论】:

  • 谢谢 Nkosi... 我需要一些时间来消化这个并将其重新用于托管服务,这给了我很多开始,谢谢。
  • @lonix 我更喜欢 @Arthur's approach 来做你想做的事。看一眼
  • 他的方法解决了我的问题,但我从你的解决方案中学到了很多东西,非常感谢!
【解决方案3】:

在 .net6 中使用 PeriodicTimer。

public class PeriodicHostedService : BackgroundService
{
    private readonly TimeSpan _period = TimeSpan.FromSeconds(5);
    private readonly ILogger<PeriodicHostedService> _logger;
    private readonly IServiceScopeFactory _factory;

    public PeriodicHostedService(
        ILogger<PeriodicHostedService> logger,
        IServiceScopeFactory factory)
    {
        _logger = logger;
        _factory = factory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using PeriodicTimer timer = new PeriodicTimer(_period);
        while (
            !stoppingToken.IsCancellationRequested &&
            await timer.WaitForNextTickAsync(stoppingToken))
        {
            //Your logic

        }
    }
}

【讨论】:

    最近更新 更多