Async timer in Scheduler Background Service

2020-07-24 04:20发布

I'm writing a hosted service in .Net-Core which runs a job in the background based off of a timer.

Currently I have to code running synchronously like so:

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

I'd like to run this Async on the timer but I don't want to run async using fire and forget because my it could cause race conditions in my code. e.g( subscribing to the timer.Elapsed event)

Is there a way I can leverage asynchronous code on a timed schedule without executing fire and forget

2条回答
看我几分像从前
2楼-- · 2020-07-24 04:31

The whole purpose of async is to not hold up the primary threads. But this is a background thread already, so it doesn't really matter - unless it's an ASP.NET Core application. That's the only time it would matter since there is a limited thread pool and exhausting it means that no more requests can be served.

If you really want to run it async, just make it async:

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

Yes, I know you say you don't want to "fire and forget", but events really are just that: they're fire and forget. So your ExecuteTask method will be called and nothing will care (or check) if it's (1) still running or (2) if it failed. That is true whether you run this async or not.

You can mitigate failures by just wrapping everything your ExecuteTask method in a try/catch block and make sure it's logged somewhere so you know what happened.

The other issue is knowing if it's still running (which, again, is a problem even if you're not running async). There is a way to mitigate that too:

private Task doWorkTask;

private void ExecuteTask(object state)
{
    doWorkTask = DoWork();
}

private async Task DoWork()
{
    //await stuff here
}

In this case, your timer just starts the task. But the difference is that you're keeping a reference to the Task. This would let you check on the status of the Task anywhere else in your code. For example, if you want to verify whether it's done, you can look at doWorkTask.IsCompleted or doWorkTask.Status.

Additionally, when your application shuts down, you can use:

await doWorkTask;

to make sure the task has completed before closing your application. Otherwise, the thread would just be killed, possibly leaving things in an inconsistent state. Just be aware that using await doWorkTask will throw an exception if an unhandled exception happened in DoWork().

It's also probably a good idea to verify if the previous task has completed before starting the next one.

查看更多
Animai°情兽
3楼-- · 2020-07-24 04:31

For those who are looking for complete example which prevents running tasks concurrently. Based on @Gabriel Luci answer and comments.

Please feel free to comment so I can correct it.

    /// <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();
        }
    }
查看更多
登录 后发表回答