Search code examples
c#asynchronousasync-awaitdeadlockdotnet-httpclient

Why doesn't calling Task<T>.Result deadlock?


After reading this post a few months ago, I became paranoid of getting the Result of a Task<T> and incessantly wrapped all of my calls to it with a ConfigureAwait(false) or Task.Run. However, for some reason the following code completes successfully:

public static void Main(string[] args)
{
    var arrays = DownloadMany();

    foreach (var array in arrays);
}

IEnumerable<byte[]> DownloadMany()
{
    string[] links = { "http://google.com", "http://microsoft.com", "http://apple.com" };

    using (var client = new HttpClient())
    {
        foreach (var uri in links)
        {
            Debug.WriteLine("Still here!");
            yield return client.GetByteArrayAsync(uri).Result; // Why doesn't this deadlock?
        }
    }
}

The code prints Still here! 3 times and then exits. Is this specific to HttpClient that it is safe to call Result on (as in the people who wrote it have peppered it with ConfigureAwait(false))?


Solution

  • Task.Result will only block in the presence of certain SynchronizationContexts. In console apps there isn't one so continuations are scheduled on the ThreadPool. Just like they are when you use ConfigureAwait(false).

    In UI threads for example there is one that schedules continuations to the single UI thread. If you wait synchronously with Task.Result using the UI thread, on a task that can only complete on the UI thread, you've got a deadlock.

    Moreover, a deadlock depends on the implementation of GetByteArrayAsync. You can only deadlock if it's an async method and its awaits don't use ConfigureAwait(false).

    If you want to you can use Stephen Cleary's AsyncContext that adds an appropriate SynchronizationContext to your console app to test whether your code can block in UI apps (or ASP.Net).


    About HttpClient's (and most of .NET's) Task-returning methods: they aren't technically async. They don't use the async and await keywords. They simply return a task. Usually a wrapper over Task.Factory.FromAsync. So it's probably "safe" blocking them anyway.