Let me just preface this question with a few things:
It works because the buttonWorking_Click
async code (DelayAsync
as well as the async
lambda passed to Task.Run
) does not have a current SynchronizationContext
, whereas the buttonDeadlock_Click
async code (DelayAsync
) does. You can observe the difference by running in the debugger and watching SynchronizationContext.Current
.
I explain the details behind the deadlock scenario in my blog post Don't Block on Async Code.
Scenario one: you are sitting at your desk. There is an inbox. It is empty. A piece of paper suddenly arrives in your inbox describing a task. You jump to your feet and start running around doing the task. But what is the task? It says to do the following:
This workflow prevents you from getting work done because the last two steps are in the wrong order.
Scenario two: you are sitting at your desk. There is an inbox. It is empty. A piece of paper suddenly arrives in your inbox describing a task. You jump to your feet and start running around doing the task. But what is the task? It says to do the following:
What does the piece of paper you gave Debbie say? It says:
This workflow still is terrible in that (1) you are sitting there doing nothing while you wait for Debbie's alarm clock to go off, and (2) you are wasting the time of two workers when you could have a single worker do all the work. Workers are expensive.
But this workflow does not prevent you from getting work done eventually. It doesn't deadlock because you are not waiting on work that you yourself are going to do in the future, you are waiting for someone else to do the work.
(I note that this is not an exact analogy for what is happening in your program, but it is close enough to get the idea across.)