0

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?

ActiveX
  • 1,064
  • 1
  • 17
  • 37
  • Does this answer your question? [How ConfigureAwait(false) Prevent Ui Deadlocks](https://stackoverflow.com/questions/65603800/how-configureawaitfalse-prevent-ui-deadlocks) – 41686d6564 stands w. Palestine Sep 15 '22 at 23:20
  • 2
    It depends on the implementation of DoWorkAsync. If inside DoWorkAsync (or inside any of the methods it calls) another async method is awaited without using ConfigureAwait(false) then and there, then the continuation of that awaited method is scheduled to run on the captured context, which is the UI thread. But it won't ever run, because GetResult() is blocking the UI thread, hence deadlock... –  Sep 15 '22 at 23:20
  • @41686d6564standsw.Palestine It doesn't, it just talks about the concepts the way I understand it. Based on that, it shouldn't dead lock. I am missing something. – ActiveX Sep 15 '22 at 23:33
  • @MySkullCaveIsADarkPlace I added the implementation of DoWorkAsync(). I know that GetResult() blocks UI, but I shouldn't be returning to the UI thread to do the continuation. – ActiveX Sep 15 '22 at 23:34
  • That's the default behaviour because the [SynchronizationContext](https://stackoverflow.com/questions/18097471/what-does-synchronizationcontext-do) of the UI thread has been set, so it will continue (queued) on the UI thread. What's the reason that you don't want to use await (async)? – Jeroen van Langen Sep 15 '22 at 23:45
  • 1
    @JeroenvanLangen "What's the reason that you don't want to use await (async)?" - This is just playing around with code and testing my knowledge of async/await. I am not sure what you mean by default behaviour when I clearly override the default behaviour with ConfigureAwait(false) – ActiveX Sep 15 '22 at 23:53

1 Answers1

4

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 2
    That's the answer I was looking for. It makes perfect sense and I see your point. Thank you so much for explaining so clearly this use case (kudos for the step by step break down). Yea, when I rewrite the call with an await (result = await obj.DoWorkAsync().ConfigureAwait(false);), the continuation runs on a thread pool thread exactly as it should because the next line "label1.Text = '';" throws an exception, not on the UI thread. – ActiveX Sep 16 '22 at 00:41
  • 1
    Oh, btw I did read both of your blog posts on async/await, but it's been a while since I read it and probably forgot some finer details. I have these bookmarked. – ActiveX Sep 16 '22 at 00:46
  • 1
    You could mention that the solution in this particular case is `await Task.Delay(3000).ConfigureAwait(false);`. The term *"transitive closure"* might be too obscure for the OP to figure it out by themselves! – Theodor Zoulias Sep 16 '22 at 02:12