What is the difference between discarding DoAwait() VS Task.Run(() => DoAwait())

Viewed 599

So let's assume I have the following async method:

public async Task DoAsync()
{
    await Task.Delay(10000);
    ExecuteContinuation();
}

Now I want to execute this task in a fire-and-forget way from ASP.NET (non-Core) Controller method (I know that's not the best idea, but that's not the scope of the question). Therefore I simply discard returned task:

_ = DoAsync();

That leads to an interesting result - ExecuteContinuation() is never executed, breakpoint is never hit. Now if I wrap it in Task.Run, everything works just fine:

_ = Task.Run(() => DoAsync());

Question is - why such behavior happens, and why ExecuteContinuation() is never executed without Task.Run?

As far as I remember, Task.Run forces the task to execute on a thread pool ignoring SynchronizationContext, but does it make any difference, considering that I never await returned task?

Another thing - _ = DoAsync(); works as expected if I do same with ASP.NET Core. I suppose because there is no SynchronizationContext, but again - I don't know why that matters in this case.

1 Answers

As far as I remember, Task.Run forces the task to execute on a thread pool ignoring SynchronizationContext, but does it make any difference, considering that I never await returned task?

Yes. It makes a difference for the await inside DoAsync. When calling it directly, it will capture the current SynchronizationContext (representing the current request). Later when that await resumes executing, it will attempt to resume on the captured context for the request that has already completed. The resulting behavior is undefined.

When calling DoAsync from Task.Run, no context is captured and DoAsync may resume executing on any thread pool thread without the context for the completed request.

Note that since this is fire-and-forget, it is inevitable that DoAsync will occasionally not finish, even when using Task.Run. Your system as a whole must properly handle that scenario. The proper solution for fire-and-forget code is a distributed architecture.

Related