简体   繁体   中英

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 (ie 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.

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 (ie 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 (eg a UI library).

You won't cause a deadlock by blocking on most out-of-the box Task -returning .NET calls because they wouldn't internally touch the SynchronizationContext that the Task was started on unless absolutely necessary (two reasons: performance and avoiding deadlocks).

What this means is that even if standard .NET calls did use async/await under the covers (i3arnon said they don't - I won't argue as I simply don't know), they would, beyond any doubt, use ConfigureAwait(false) unless capturing the context is definitely required.

But that's the .NET framework. As for your own code, you will observe a deadlock if you call wrapperMethod().Wait() (or Result ) in your client (provided that you're running with a non-null SynchronizationContext.Current - if you're using Windows Forms, this will definitely be the case). Why? Because you're flaunting async/await best practices by not using ConfigureAwait(false) on your awaitables inside async methods that do not interact with the UI, causing the state machine to generate continuations that unnecessarily execute on the original SynchronizationContext .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM