Search code examples
c#asynchronousasync-awaitdeadlocksynchronizationcontext

Async-Await no deadlock where a deadlock is expected


It is known that synchronous waiting on an async method leads to deadlocks (see, for example Don't Block on Async Code)

I have the following code in an event handler for a button-click in a Windows Forms application (i.e. the code is invoked with a UI SynchronizationContext installed).

var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, new Uri("http://www.google.com"));
Task<HttpResponseMessage> t = client.SendAsync(request);
t.Wait();
var response = t.Result;

I fully expected the code to deadlock on clicking the button. However, what I actually see is synchronous waiting - the dialog becomes unresponsive for a while, and then accepts events as usual. I consistently see deadlocks when I try to synchronously wait on client async methods. However, synchronously waiting on library async methods like SendAsync or ReadAsByteArrayAsync seems not to deadlock. Can someone explain this behaviour?

Don't implementations of async methods in .NET libraries use await statements internally, so that the continuations have to be marshalled back to the original SynchronizationContext?

Note: If I define a client method, say

public async Task<byte[]> wrapperMethod()
{
    var client = new HttpClient();
    var request = new HttpRequestMessage(HttpMethod.Get, new Uri("http://www.google.com"));
    var response = await client.SendAsync(request);
    return await response.Content.ReadAsByteArrayAsync();
}

and then say byte[] byteArray = wrapperMethod().Result; in the button click handler, I do obtain a deadlock.


Solution

  • Don't implementations of async methods in .NET libraries use await statements internally?

    Generally, no. I have yet to see a single implementation in the .NET framework that uses async-await internally. It does use tasks and continuations but not the compiler magic the async and await keywords bring.

    Using async-await is simple as the code looks synchronous but actually runs asynchronously. But that simplicity has a very small price in performance.

    For most consumers this prices is worth paying, but the framework itself tries to be as performant as possible.

    However, synchronously waiting on library async methods like SendAsync or ReadAsByteArrayAsync seems not to deadlock.

    The deadlock is a result of the default behaviour of await. When you await an uncompleted task the SynchronizationContext is captured and when it's completed the continuation is resumed on that SynchronizationContext (if it exists). When there's no async, await, captured SynchronizationContext, etc. this kind of deadlock can't happen.

    HttpClient.SendAsync specifically uses TaskCompletionSource to return a task without marking the method as async. You can see that in the implementation on github here.

    Most task-returning methods added to existing classes for async-await simply build a task using the already existing asynchronous API (i.e. BeginXXX/EndXXX). For example this is TcpClient.ConnectAsync:

    public Task ConnectAsync(IPAddress address, int port)
    {
        return Task.Factory.FromAsync(BeginConnect, EndConnect, address, port, null);
    }
    

    When you do use async-await though you avoid the deadlock by using ConfigureAwait(false) when you don't need to capture the SynchronizationContext. It's recommended that libraries should alway use it unless the context is needed (e.g. a UI library).