Home > Software engineering >  Why does ConfigureAwait causes a deadlock in the following scenario?
Why does ConfigureAwait causes a deadlock in the following scenario?

Time:09-23

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 awaits 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();
  1. Asynchronous methods begin executing synchronously, just like any other method. In this case, DoWorkAsync is called on the UI thread.
  2. DoWorkAsync calls Task.Delay (also synchronously).
  3. Task.Delay returns a task that is not complete; it will complete in 3 seconds. Still synchronous, and on the UI thread.
  4. The await in DoWorkAsync 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.
  5. The calling code calls ConfigureAwait(false) on the returned task. This essentially has no effect.
  6. The calling code calls GetAwaiter().GetResult() on the (configured) returned task. This blocks the UI thread waiting on the task.
  7. 3 seconds later, the task returned by Task.Delay completes, and the continuation of DoWorkAsync is scheduled to the context captured by its await - the UI context.
  8. 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.

  • Related