调度程序后台服务中的异步计时器

Posted

技术标签:

【中文标题】调度程序后台服务中的异步计时器【英文标题】:Async timer in Scheduler Background Service 【发布时间】:2019-05-19 13:16:36 【问题描述】:

我正在 .Net-Core 中编写一个托管服务,它根据计时器在后台运行作业。

目前我必须像这样同步运行代码:

public override Task StartAsync(CancellationToken cancellationToken)

    this._logger.LogInformation("Timed Background Service is starting.");

    this._timer = new Timer(ExecuteTask, null, TimeSpan.Zero,
        TimeSpan.FromSeconds(30));

    return Task.CompletedTask;


private void ExecuteTask(object state)

    this._logger.LogInformation("Timed Background Service is working.");
    using (var scope = _serviceProvider.CreateScope())
    
        var coinbaseService = scope.ServiceProvider.GetRequiredService<CoinbaseService>();
        coinbaseService.FinalizeMeeting();
    

我想在计时器上运行此异步,但我不想使用 fire 运行异步并忘记,因为它可能会导致我的代码中出现竞争条件。 例如(订阅timer.Elapsed 事件)

有没有一种方法可以在定时调度上利用异步代码而无需执行触发并忘记

【问题讨论】:

你为什么不能直接private async Task ExecuteTask(object state) @zaitsman ExecuteTask 被定时器触发,我不想触发并忘记我的任务,但在调试后我认为它无论如何都会发生 【参考方案1】:

对于那些正在寻找防止同时运行任务的完整示例的人。 基于@Gabriel Luci 的回答和 cmets。

请随时发表评论,以便我更正。

    /// <summary>
    /// Based on Microsoft.Extensions.Hosting.BackgroundService  https://github.com/aspnet/Extensions/blob/master/src/Hosting/Abstractions/src/BackgroundService.cs
    /// Additional info: - https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.2&tabs=visual-studio#timed-background-tasks
    ///                  - https://***.com/questions/53844586/async-timer-in-scheduler-background-service
    /// </summary>

    public abstract class TimedHostedService : IHostedService, IDisposable
    
        private readonly ILogger _logger;
        private Timer _timer;
        private Task _executingTask;
        private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();

        public TimedHostedService(ILogger<TimedHostedService> logger)
        
            _logger = logger;
        

        public Task StartAsync(CancellationToken cancellationToken)
        
            _logger.LogInformation("Timed Background Service is starting.");

            _timer = new Timer(ExecuteTask, null, TimeSpan.FromSeconds(30), TimeSpan.FromMilliseconds(-1));

            return Task.CompletedTask;
        

        private void ExecuteTask(object state)
        
            _timer?.Change(Timeout.Infinite, 0);
            _executingTask = ExecuteTaskAsync(_stoppingCts.Token);
        

        private async Task ExecuteTaskAsync(CancellationToken stoppingToken)
        
            await RunJobAsync(stoppingToken);
            _timer.Change(TimeSpan.FromSeconds(30), TimeSpan.FromMilliseconds(-1));
        

        /// <summary>
        /// This method is called when the <see cref="IHostedService"/> starts. The implementation should return a task 
        /// </summary>
        /// <param name="stoppingToken">Triggered when <see cref="IHostedService.StopAsync(CancellationToken)"/> is called.</param>
        /// <returns>A <see cref="Task"/> that represents the long running operations.</returns>
        protected abstract Task RunJobAsync(CancellationToken stoppingToken);

        public virtual async Task StopAsync(CancellationToken cancellationToken)
        
            _logger.LogInformation("Timed Background Service is stopping.");
            _timer?.Change(Timeout.Infinite, 0);

            // Stop called without start
            if (_executingTask == null)
            
                return;
            

            try
            
                // Signal cancellation to the executing method
                _stoppingCts.Cancel();
            
            finally
            
                // Wait until the task completes or the stop token triggers
                await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
            

        

        public void Dispose()
        
            _stoppingCts.Cancel();
            _timer?.Dispose();
        
    

【讨论】:

【参考方案2】:

async 的全部目的是不阻塞主线程。但这已经是一个后台线程,所以它并不重要——除非它是一个 ASP.NET Core 应用程序。这是唯一重要的时间,因为线程池有限,耗尽它意味着无法处理更多请求。

如果你真的想运行它async,就让它async

private async void ExecuteTask(object state)

    //await stuff here

是的,我知道你说你不想“一劳永逸”,但事件确实就是这样:它们就是一劳永逸。因此,您的 ExecuteTask 方法将被调用,并且不会关心(或检查)它是否(1)仍在运行或(2)是否失败。 无论您是否运行此async,都是如此。

您可以通过将 ExecuteTask 方法的所有内容包装在 try/catch 块中并确保它记录在某处以便您知道发生了什么来缓解故障。

另一个问题是知道它是否仍在运行(即使您没有运行async,这也是一个问题)。还有一种方法可以缓解这种情况:

private Task doWorkTask;

private void ExecuteTask(object state)

    doWorkTask = DoWork();


private async Task DoWork()

    //await stuff here

在这种情况下,您的计时器只是启动任务。但不同之处在于您保留了对Task 的引用。这可以让您检查代码中其他任何地方的Task 的状态。例如,如果要验证是否完成,可以查看doWorkTask.IsCompleteddoWorkTask.Status

此外,当您的应用程序关闭时,您可以使用:

await doWorkTask;

在关闭您的应用程序之前确保任务已完成。否则,线程将被杀死,可能会使事情处于不一致的状态。请注意,如果 DoWork() 中发生未处理的异常,使用 await doWorkTask 将引发异常。

在开始下一个任务之前验证上一个任务是否已完成也是一个好主意。

【讨论】:

异步释放你的线程,我想释放我的线程,另外我的服务代码是异步编写的,我不想同步运行 它只有助于释放 ASP.NET Core 中的线程。否则,就无所谓了。我更新了我的答案以讨论如何做到这一点以及如何处理一些问题,这些问题实际上是异步与否的问题。 谢谢,我会将此标记为已解决,因为事实证明,如果我使用 fire 并忘记它会同时运行我的任务:/,但这与旧版本无关.net 的。特别是在共享托管上,因为可以通过 IIS 限制每个进程的线程? async 使用硬件中断,因此它会在网络和 I/O 操作期间释放线程。 你是对的:如果你在一个网络应用程序中运行它,那么是的,你应该运行它async(不管它是不是即发即弃)。 为了防止同时运行任务(我假设您的意思是计时器的下一次迭代开始而前一个尚未完成?)然后您可以手动重新启动计时器而不是让它自行重置.通过将其声明为 new Timer(ExecuteTask, null, TimeSpan.Zero, -1); 来做到这一点,然后,当您的任务完成后,调用 _timer.Change(TimeSpan.FromSeconds(30), -1) 告诉它开始倒计时。【参考方案3】:

这里是基于之前回复的改进版本。改进:

    捕获任务执行期间可能出现的异常,不会阻止执行下一个任务。 为执行范围的每个任务创建一个范围,因此您可以在 RunJobAsync 中访问任何范围内的服务 您可以在继承的类中指定间隔和初始任务执行时间。

访问范围服务示例

    protected override async Task RunJobAsync(IServiceProvider serviceProvider, CancellationToken stoppingToken)
    
            DbContext context = serviceProvider.GetRequiredService<DbContext>();
    

源代码:

public abstract class TimedHostedService : IHostedService, IDisposable

    private readonly ILogger _logger;
    private Timer _timer;
    private Task _executingTask;
    private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();

    IServiceProvider _services;
    public TimedHostedService(IServiceProvider services)
    
        _services = services;
        _logger = _services.GetRequiredService<ILogger<TimedHostedService>>();
        
    

    public Task StartAsync(CancellationToken cancellationToken)
    
        _timer = new Timer(ExecuteTask, null,FirstRunAfter, TimeSpan.FromMilliseconds(-1));

        return Task.CompletedTask;
    

    private void ExecuteTask(object state)
    
        _timer?.Change(Timeout.Infinite, 0);
        _executingTask = ExecuteTaskAsync(_stoppingCts.Token);
    

    private async Task ExecuteTaskAsync(CancellationToken stoppingToken)
    
        try
        
            using (var scope = _services.CreateScope())
            
                await RunJobAsync(scope.ServiceProvider, stoppingToken);
            
        
        catch (Exception exception)
        
            _logger.LogError("BackgroundTask Failed", exception);
        
        _timer.Change(Interval, TimeSpan.FromMilliseconds(-1));
    

    /// <summary>
    /// This method is called when the <see cref="IHostedService"/> starts. The implementation should return a task 
    /// </summary>
    /// <param name="serviceProvider"></param>
    /// <param name="stoppingToken">Triggered when <see cref="IHostedService.StopAsync(CancellationToken)"/> is called.</param>
    /// <returns>A <see cref="Task"/> that represents the long running operations.</returns>
    protected abstract Task RunJobAsync(IServiceProvider serviceProvider, CancellationToken stoppingToken);
    protected abstract TimeSpan Interval  get; 
    
    protected abstract TimeSpan FirstRunAfter  get; 
    
    public virtual async Task StopAsync(CancellationToken cancellationToken)
    
        _timer?.Change(Timeout.Infinite, 0);

        // Stop called without start
        if (_executingTask == null)
        
            return;
        

        try
        
            // Signal cancellation to the executing method
            _stoppingCts.Cancel();
        
        finally
        
            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
        

    

    public void Dispose()
    
        _stoppingCts.Cancel();
        _timer?.Dispose();
    

【讨论】:

改进建议:Timeout.InfiniteTimeSpan 而不是TimeSpan.FromMilliseconds(-1)

以上是关于调度程序后台服务中的异步计时器的主要内容,如果未能解决你的问题,请参考以下文章

如何按需运行后台服务——而不是在应用程序启动或计时器上

在后台永远运行Swift 2.0应用程序以更新服务器的位置

C#如何在BackgroundWorker 后台线程中使用定时器?

计时器与重复后台工作人员

当应用程序处于后台并且网络连接丢失时,位置更新计时器不起作用?

从 Android 后台服务问题获取当前位置