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

Async timer in Scheduler Background Service

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

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

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 事件)

有没有一种方法可以在不执行即发即弃的情况下按定时计划利用异步代码

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

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

private async void ExecuteTask(object state)
{
    //await stuff here
}

是的,我知道你说你不想 "fire and forget",但 事件 真的就是这样:它们是火了,忘了。因此,您的 ExecuteTask 方法将被调用,并且不会关心(或检查)它是否 (1) 仍然 运行ning 或 (2) 如果失败。 无论你是否运行这个async都是如此。

您可以通过将 ExecuteTask 方法的所有内容包装在 try/catch 块中来减少失败,并确保将其记录在某处以便您知道发生了什么。

另一个问题是知道它是否仍然 运行ning(即使你不是 运行ning 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 将引发异常

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

对于那些正在寻找阻止 运行 任务并发的完整示例的人。 基于@Gabriel Luci 的回答和评论。

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

    /// <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
    ///                  - 
    /// </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();
        }
    }

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

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

访问范围内的服务示例

    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();
    }
}