在ASP.NET Core中每5分钟运行异步托管服务

19
ASP.NET Core的文档为后台服务提供了许多实现示例。其中有一个例子是使用定时器启动服务,但它是同步的。还有另一个例子是异步的,用于使用作用域依赖项启动服务。
我需要同时满足这两种需求:每5分钟启动一次服务,并且需要使用作用域依赖项。但是没有对应的示例。我结合了两个示例,但不确定如何安全地使用异步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。有没有一种安全的方法来调用异步服务?

计时器需要一个同步回调函数,所以问题在于await。有没有一种安全的方法来调用异步服务?


@TheodorZoulias 我只是从原始示例中复制粘贴了这个内容,重叠执行是一个很好的点子(谢谢!),但最好单独提出一个问题,以免使这个问题过于复杂。我可能会使用关键块来确保一次只执行一个操作。 - lonix
1
你可能会发现这个有用:按指定间隔定期运行异步方法。在事件处理程序内添加关键部分可能会导致ThreadPool长期饱和,因为越来越多的线程可能会在计时器滴答声响起时被阻塞。 - Theodor Zoulias
2
@lonix 是的,Artur的答案可以防止重叠,因为它不涉及基于事件的Timer,该计时器在ThreadPool上调用事件处理程序。 - Theodor Zoulias
2
@lonix "选项2" 也可以防止重叠。PeriodicTimer 是一个可等待计时器,而不是基于事件的计时器。 - Theodor Zoulias
1
@TheodorZoulias,您的评论和下面的答案一样有帮助,谢谢! - lonix
显示剩余5条评论
2个回答

36
使用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)
        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            // do async work
            // ...as above
        }
    }
}

2
你在问题中使用了asp.net-core-5标签,但请注意.NET 5.0将于2022年8月5日 停止支持,所以你最好升级到6.0版本并使用PeriodicTimer - Artur
我喜欢这种方法。它比我建议的要简单得多。 - Nkosi
谢谢。要像我上面展示的那样使用作用域依赖项,是否可以使用您的解决方案以相同的方式完成?即在 while 循环内部创建一个作用域,并在每个 tick 中从中解析服务是“安全”的。 - lonix
是的,同样的方式。 - Artur
你实际上是如何运行的?是什么调用了ExecuteAsync? - user1034912
显示剩余5条评论

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
@lonix,我更喜欢@Arthur的方法,这是你想要做的。看一下。 - Nkosi
他的方法解决了我的问题,但我从你的解决方案中学到了很多,非常感谢! - lonix

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接