Search code examples
c#asynchronoustimer.net-corebackground-service

Async timer in Scheduler Background Service


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


Solution

  • 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.