1

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");
        }
    }
Tiago
  • 68
  • 6
  • I regards to the problem with `WaitingForActivation` you may want to read [this](https://stackoverflow.com/a/20831464/6560579) answer – Ackdari Sep 23 '19 at 12:51
  • 1
    This is an MVC controller - it should be very easy to mark the method as `async`. What is preventing you from doing that? – mason Sep 23 '19 at 13:02
  • This is just an example, in our production code it would take a lot of effort to change the framework, so that it allows an async method. – Tiago Sep 23 '19 at 13:06
  • 1
    never break the async change up to the highest point of the calling hierarchy. [dont-block-on-async-code](https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html) – Mong Zhu Sep 23 '19 at 13:10
  • 1
    In your comment you must include the name of the user, prepended with an @, so that he receives a notification. Like this: @MongZhu if you think... – Theodor Zoulias Sep 23 '19 at 13:34
  • @MongZhu No, unfortunately my question is not answered. I did already know how to solve my problem in multiple ways, and have already read the blog post of Stephen Cleary last friday. Still, I do not have an answer to the question why in my example task2.Result results in a deadlock and task3.Result does not. I would like to understand the reason for task3.Result to work. Right now I might want to use task3.Result as my solution, knowing the reason it works might change my mind more in to the direction of "don't block on async code". – Tiago Sep 23 '19 at 14:23

2 Answers2

2

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.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Thanks for your answer, it helped me to understand the context part of the problem. – Tiago Sep 24 '19 at 06:02
  • @Tiago you are welcome! I added one more paragraph to clarify a point. I hope it makes sense because all this machinery of tasks makes it difficult to be unambiguous about what task you are talking each time. – Theodor Zoulias Sep 24 '19 at 06:09
1

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.

Update:

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.

Community
  • 1
  • 1
Ackdari
  • 3,222
  • 1
  • 16
  • 33
  • That is what I have expected, but the task2.Result example does not work either, which should work given your explanation. Can you explain, why the task2 example results in a deadlock too? – Tiago Sep 23 '19 at 13:05