Reputation: 68
TLDR: In my example (ASP.NET) below why does task1.Result and task2.Result result in a deadlock and task3.Result does not?
We have a class in our code, where I cannot easily mark the method as async and keep the code from blocking while waiting for an async task to finish. The reason is that our framework does not support that. Therefore I tried to find a solution with task.Result and got some deadlocks. Luckily I found a solution (see task3). Now I tried to find out, what is the difference between task1, task2 and task3 to understand why the first two result in a deadlock and the third does not.
Using the debugger I did not see any difference like task3 being run before task3.Result is called. It is still in WaitingForActivation state like the other two. Can anyone explain to me, how task3 can work?
public class HomeController : Controller
{
public ActionResult GetSomething()
{
var task1 = GetSomethingAsync();
var task2 = Task.Run(async () => await task1);
var task3 = Task.Run(async () => await GetSomethingAsync());
return Content(task1.Result);
// return Content(task2.Result);
// return Content(task3.Result);
}
private static async Task<string> GetSomethingAsync()
{
return await Task.Run(() => "something");
}
}
Upvotes: 1
Views: 191
Reputation: 43911
The root of the problem is the await
here:
private static async Task<string> GetSomethingAsync()
{
return await Task.Run(() => "something");
}
The context is captured before entering the awaiting, and the context is the main thread. So after the awaiting the context must be restored, so the continuation is scheduled to run in the main thread. Since the main thread is blocked at this point, it can't process the continuation, hence the deadlock.
To prevent the capturing of the context you could configure this await
with ConfigureAwait(false)
.
Update: By continuation I mean the code inside GetSomethingAsync
that follows after the awaiting. Although there is no code after that, it seems that the compiler does bother to create an actual continuation for this no-op part of the method (otherwise a deadlock should not occur in your example).
It should be noted that the compiler transforms an async
method to a Task
that consists of multiple mini-tasks. Every await
encountered in the path of the execution causes the creation of a mini-task that is the continuation of the awaited task. All these mini-tasks are completed one after the other, and the completion of the last one signals the completion of the "master-task" that is returned by the async
method.
Upvotes: 2
Reputation: 3508
What ever GetSomethingAsync()
does it is done by the calling thread until some operation (OP) can not be completed right away (for example io) then the controlflow is given to the calling function but.
If you then access the Result
property on the returned Task
object the thread will be blocked.
This leads to the problem that even is the OP is finished the thread will not know about it because he is busy waiting for the completion of the Task
If however you let GetSomethingAsync()
be exectuted by some thread-pool thread (which Task.Run(...)
does) the thread-pool thread can finish the OP and the calling thread can be notified that the Task
has completed.
Your second approch does not work because the task was still started on your main thread. If you have this methode
public static async Task DoStuffAsync()
{
Console.WriteLine($"Doing some stuff1 on thread {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(50);
Console.WriteLine($"Doing some stuff2 on thread {Thread.CurrentThread.ManagedThreadId}");
}
and run this code in an appilcation with an SynchronizationContext
var task = DoStuffAsync();
Console.WriteLine($"Doing main stuff on thread {Thread.CurrentThread.ManagedThreadId}");
await Task.Run(async () => await task);
It will output something like:
Doing some stuff1 on thread 1
Doing main stuff on thread 1
Doing some stuff2 on thread 1
So with the code line Task.Run(async () => await task)
you only achieved that a thread-pool thread waits on the completion of your original Task
, but this in turn creates a new Task
that if not handeld by awaiting it causes a deadlock.
Upvotes: 1