Search code examples
c#asp.net-coreasync-awaitbackground-serviceihostedservice

BackgroundService never started/stopped if no await done


On ASP.NET Core I'm observing a strange behavior, that was actually reported in BackgroundService not shutting down, stoppingToken never set with .net core generic host but without the root cause ever being found

I'm creating the following BackgroundService task, registered as a HostedService :

The only method is implemented like so :

 protected override async Task ExecuteAsync(CancellationToken cancellationToken)
 {
     while (!cancellationToken.IsCancellationRequested )
         Console.WriteLine("running");
 }

If I try to Ctrl+C or kill -15 this, it is not stopping.

If I change the function like this:

protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested )
    { 
        Console.WriteLine("running");
        await Task.Delay(1);
    }
}

This works: if I try to Ctrl+C this program, the cancellation token is set, and I exit.

If I go back to the version that doesn't work and pause it, I see that even though I'm in the ExecuteAsync method, the frame below it is StartAsync(), which in that case never completed!

The code I see for StartAsync() (from the framework), is this one:

public virtual Task StartAsync(CancellationToken cancellationToken)
{
    // Store the task we're executing
    _executingTask = ExecuteAsync(_stoppingCts.Token);

    // If the task is completed then return it, this will bubble cancellation and failure to the caller
    if (_executingTask.IsCompleted)
    {
        return _executingTask;
    }

    // Otherwise it's running
    return Task.CompletedTask;
}

So, what is going on here ?

I have 2 questions:

  1. Why is ExecuteAsync not run from the start in another thread on the thread pool ? I would assume the call to _executingTask = ExecuteAsync(_stoppingCts.Token); would immediately return but apparently it's not the case, and it's waiting for the first await to execute the line just after

  2. Is my code of BackgroundService incorrect ? AFAIK it's a legal use-case to use purely blocking code in async function, it shouldn't lead to the whole application forever blocking


Solution

  • This is pretty much covered in the docs:

    ExecuteAsync(CancellationToken)is called to run the background service. The implementation returns a System.Threading.Tasks.Task that represents the entire lifetime of the background service. No further services are started until ExecuteAsync becomes asynchronous, such as by calling await. Avoid performing long, blocking initialization work in ExecuteAsync.

    The simple fix is to just call await Task.Yield() at the start of ExecuteAsync:

    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        await Task.Yield();
        // ... rest of the code
    }
    

    Note that your initial implementation should produce a warning complaining about lacking await which is hinting that you are potentially doing something not entirely correct.