In the context of ASP.NET, why doesn't Task.Run(...).Result deadlock when calling an async method?
Asked Answered
O

2

5

I created a simple WebApi project with a single controller and a single method:

public static class DoIt
{
    public static async Task<string> GetStrAsync(Uri uri)
    {
        using (var client = new HttpClient())
        {
            var str = await client.GetStringAsync(uri);
            return str;
        }
    }
}

public class TaskRunResultController : ApiController
{
    public string Get()
    {
        var task = Task.Run(() =>
            DoIt.GetStrAsync(new Uri("http://google.com"))
        );
        var result = task.Result;

        return result;
    }
}

I have a good understanding of async/await and tasks; almost religiously following Stephen Cleary. Just the existence of .Result makes me anxious, and I expect this to deadlock. I understand that the Task.Run(...) is wasteful, causing a thread to be occupied while waiting for the async DoIt() to finish.

The problem is this is not deadlocking, and it's giving me heart palpitations.

I see some answers like https://mcmap.net/q/931163/-calling-an-async-method-using-a-task-run-seems-wrong, and I've also observed that SynchronizationContext.Current is null when the lambda is executing. However, there are similar questions to mine asking why the above code does deadlock, and I've seen deadlocks occur in cases where ConfigureAwait(false) is used (not capturing the context) in conjunction with .Result.

What gives?

Outdoor answered 29/6, 2017 at 0:12 Comment(0)
V
8

Result by itself isn't going to cause a deadlock. A deadlock is when two parts of the code are both waiting for each other. A Result is just one wait, so it can be part of a deadlock, but it doesn't necessarily always cause a deadlock.

In the standard example, the Result waits for the task to complete while holding onto the context, but the task can't complete because it's waiting for the context to be free. So there's a deadlock - they're waiting for each other.

ASP.NET Core does not have a context at all, so Result won't deadlock there. (It's still not a good idea for other reasons, but it won't deadlock).

The problem is this is not deadlocking, and it's giving me heart palpitations.

This is because the Task.Run task does not require the context to complete. So calling Task.Run(...).Result is safe. The Result is waiting for the task to complete, and it is holding onto the context (preventing any other parts of the request from executing in that context), but that's OK because the Task.Run task doesn't need the context at all.

Task.Run is still not a good idea in general on ASP.NET (for other reasons), but this is a perfectly valid technique that is useful from time to time. It won't ever deadlock, because the Task.Run task doesn't need the context.

However, there are similar questions to mine asking why the above code does deadlock,

Similar but not exact. Take a careful look at each code statement in those questions and ask yourself what context it runs on.

and I've seen deadlocks occur in cases where ConfigureAwait(false) is used (not capturing the context) in conjunction with .Result.

The Result/context deadlock is a very common one - probably the most common one. But it's not the only deadlock scenario out there.

Here's an example of a much more difficult deadlock that can show up if you block within async code (without a context). This kind of scenario is much more rare, though.

Vogue answered 30/6, 2017 at 0:35 Comment(2)
Thanks, Stephen. I suspected the answer was essentially "this is because the Task.Run task does not require the context to complete," but was hesitant to call this "safe." I understand the other repercussions. Thanks for the blocking example; as it's now 2017, is the rarer scenario still valid? You also state this will not occur in GUI or ASP.NET code - in which scenario does it deadlock?Outdoor
Yes. await still uses ExecuteSynchronously, so the more rare scenario is still valid. In this case it's a deadlock where two thread pool threads are waiting for each other.Vogue
F
1

I'll give you as short answer the other way around: how to produce a deadlock:

Lets start in a Click handler that is executing synchronous and offloading some async call to a separate Task, then waiting for the result

private void MenuItem_Click(object sender, RoutedEventArgs e)
{
    var t = Task.Run(() => DeadlockProducer(sender as MenuItem));
    var result = t.Result;
}

private async Task<int> DeadlockProducer(MenuItem sender)
{
    await Task.Delay(1);
    Dispatcher.Invoke(() => sender.Header = "Life sucks");
    return 0;
}

The async method is doing another bad thing: it tries to bring some UI change synchronously, but the UI thread is still waiting for the task result...

So basically, Task.Run is half way out and will work for a lot of well formed async code, but there are ways to break it, so its not a reliable solution.

This sample code was written in context of WPF, but I guess ASP.Net could be abused to produce similar behavior.

Forsberg answered 29/6, 2017 at 6:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.