Scheduler后台服务中的异步计时器

时间:2018-12-19 04:27:02

标签: c# asynchronous timer .net-core background-service

我正在.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();
    }
}

我想在计时器上运行此Async,但是我不想使用fire来运行async并忘记了,因为这可能会导致代码中出现竞争情况。 例如(订阅timer.Elapsed事件)

有没有一种方法可以按时利用时间表上的异步代码,而无需执行触发操作而忘记

3 个答案:

答案 0 :(得分:1)

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;

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

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

答案 1 :(得分:0)

对于那些正在寻找可以防止同时运行任务的完整示例的人。 基于@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
    ///                  - https://stackoverflow.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 :(得分:0)

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

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

访问范围服务示例

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