I have a WinForms application with a single button and a single button Click event handler. In the event handler, I have this code:
NOTE: DoWorkAsync()
returns a Task<int>
.
var result = obj.DoWorkAsync().ConfigureAwait(false).GetAwaiter().GetResult();
Implementation of DoWorkAsync():
public async Task<int> DoWorkAsync()
{
await Task.Delay(3000);
return 200;
}
This code will deadlock but I am not sure why. I have configured the task not to continue on the UI thread after GetResult()
returns. So why does the code deadlock instead of running the next line of code?
CodePudding user response:
I have configured the task
ConfigureAwait
is for configuring awaits, not tasks. There's nothing that await
s the result of the ConfigureAwait
, so that's an indication that it's being misused here.
I have configured the task not to continue on the UI thread after GetResult() returns.
Not really. As explained above, the ConfigureAwait
has no effect here. More broadly, the code is (synchronously) blocking on the task, so the UI thread is blocked and then will resume executing after GetResult()
returns.
This code will deadlock but I am not sure why.
Walk through it step by step. Read my blog post on async/await if you haven't already done so.
Note that this:
var result = obj.DoWorkAsync().ConfigureAwait(false).GetAwaiter().GetResult();
is pretty much the same as this:
var doWorkTask = obj.DoWorkAsync();
var result = doWorkTask.ConfigureAwait(false).GetAwaiter().GetResult();
- Asynchronous methods begin executing synchronously, just like any other method. In this case,
DoWorkAsync
is called on the UI thread. DoWorkAsync
callsTask.Delay
(also synchronously).Task.Delay
returns a task that is not complete; it will complete in 3 seconds. Still synchronous, and on the UI thread.- The
await
inDoWorkAsync
checks to see if the task is complete. Since it is not complete,await
captures the current context (the UI context), pauses the method, and returns an incomplete task. - The calling code calls
ConfigureAwait(false)
on the returned task. This essentially has no effect. - The calling code calls
GetAwaiter().GetResult()
on the (configured) returned task. This blocks the UI thread waiting on the task. - 3 seconds later, the task returned by
Task.Delay
completes, and the continuation ofDoWorkAsync
is scheduled to the context captured by itsawait
- the UI context. - Deadlock. The continuation is waiting for the UI thread to be free, and the UI thread is waiting for the task to complete.
In summary, a top-level ConfigureAwait(false)
is insufficient. There are several approaches to sync-over-async code, with the ideal being "don't do it at all; use await
instead". If you want to directly block, you need to apply ConfigureAwait(false)
on every await
on the method that is called (DoWorkAsync
in this case), as well as the transitive closure of all methods called starting from there.
Clearly, this is a maintenance burden, and occasionally impossible (i.e., third-party libraries missing a ConfigureAwait(false)
), and that's why I don't usually recommend this approach.