Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mixing async/await with Result

Let me just preface this question with a few things:

  1. I've read several SO questions saying that you should not do this (such as How to safely mix sync and async code)
  2. I've read Async/Await - Best Practices in Asynchronous Programming again saying you shouldn't do this

So I do know that this is not a best practice, and don't need anyone telling me such. This is more of a "why does this work" question.

With that out of the way, here is my question:

I've written a small GUI application that has 2 buttons and a status label. One of the buttons will reproduce the deadlock issue with sync and async 100% of the time. The other button calls the same async method but it is wrapped in a Task, this one works. I know this is not a good coding practice, but I want to understand why it doesn't have the same deadlock issue. Here is the code:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private async Task<string> DelayAsync()
    {
        await Task.Delay(1000);
        return "Done";
    }

    private void buttonDeadlock_Click(object sender, EventArgs e)
    {
        labelStatus.Text = "Status: Running";

        // causes a deadlock because of mixing sync and async code
        var result = DelayAsync().Result;
        // never gets here
        labelStatus.Text = "Status: " + result;
    }

    private void buttonWorking_Click(object sender, EventArgs e)
    {
        labelStatus.Text = "Status: Running";
        string result = null;

        // still technically mixes sync and async, but works, why?
        result = Task.Run(async () =>
        {
            return await DelayAsync();
        }).Result;

        labelStatus.Text = "Status: " + result;
    }
}
like image 226
Middas Avatar asked Jul 28 '16 17:07

Middas


2 Answers

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.

like image 83
Stephen Cleary Avatar answered Oct 23 '22 14:10

Stephen Cleary


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:

  • Change the whiteboard to say "running" -- OK, you do that.
  • Set your alarm clock for an hour later. OK, you do that.
  • Create a new piece of paper that says "when the alarm goes off, write the word DONE on the whiteboard". Put it in your inbox. You do that.
  • Do nothing else until the word DONE is written on the whiteboard.
  • Go back to your desk and wait for the next task to arrive in the inbox.

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:

  • Change the whiteboard to say "running" -- OK, you do that.
  • Give this other piece of paper to Debbie in the next cubicle. OK, you do that.
  • Do nothing until someone tells you that the sub-task is DONE.
  • When that happens, write the word DONE on your whiteboard.
  • Go back to your desk.

What does the piece of paper you gave Debbie say? It says:

  • Set your alarm clock for an hour later. OK, she does that.
  • When the alarm goes off, put a piece of paper in your inbox saying to tell Middas that you're done.

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.)

like image 31
Eric Lippert Avatar answered Oct 23 '22 15:10

Eric Lippert