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:
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 afterIs 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
CodePudding user response:
This is pretty much covered in the docs:
ExecuteAsync(CancellationToken)
is called to run the background service. The implementation returns aSystem.Threading.Tasks.Task
that represents the entire lifetime of the background service. No further services are started untilExecuteAsync
becomes asynchronous, such as by callingawait
. Avoid performing long, blocking initialization work inExecuteAsync
.
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.