3

I was experimenting with async-await and I came across this rather strange behavior, at least to me.
I created three methods that simulated long running tasks.
Consider the two button click handlers:
For button1_click the time elapsed was around 6000ms while button2_click around 3000ms.
I am not able to wrap my head around why this happened, i.e. 6000ms vs 3000ms.

    private async Task<string> TaskOne()
    {
        await Task.Delay(1000);
        return "task one";
    }

    private async Task<string> TaskTwo()
    {
        await Task.Delay(2000);
        return "task two";
    }

    private async Task<string> TaskThree()
    {
        await Task.Delay(3000);
        return "task three";
    }
    
    //time elapsed = 6000+ms
    private async void button1_Click(object sender, EventArgs e)
    {
        var watch = new Stopwatch();
        watch.Start();

        await TaskOne();
        await TaskTwo();
        await TaskThree();
        
        watch.Stop();
        textBox3.Text = watch.ElapsedMilliseconds.ToString();
    }

    //time elapsed = 3000+ms
    private async void button2_Click(object sender, EventArgs e)
    {
        var watch = new Stopwatch();
        watch.Start();

        var taskOne = TaskOne();
        var taskTwo = TaskTwo();
        var taskThree = TaskThree();

        await taskOne;
        await taskTwo;
        await taskThree;
        
        watch.Stop();
        textBox3.Text = watch.ElapsedMilliseconds.ToString();
    }
HLLau
  • 39
  • 3
  • 2
    In the first case, you don't start the next task until the previous one completes. In the second case, you start all the tasks without awaiting in between so they run in parallel. – Raymond Chen Nov 20 '20 at 02:48
  • 2
    I have an answer that explains this on another question: https://stackoverflow.com/a/44204614/20471 – Joe Phillips Nov 20 '20 at 03:04
  • 1
    This article is a good read on this subject: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/ – devNull Nov 20 '20 at 04:30

2 Answers2

7

In this case:

 await TaskOne();
 await TaskTwo();
 await TaskThree();

TaskTwo() can't start until TaskOne() completes because you're awaiting it. Likewise TaskThree() can't start until TaskTwo() is completed because of the await.

In the next:

var taskOne = TaskOne();
var taskTwo = TaskTwo();
var taskThree = TaskThree();

await taskOne;
await taskTwo;
await taskThree;

You're starting all 3 tasks at the same time and then awaiting them. So that's why it's only taking as long as the longest running task. You'd be surprised at how many people don't understand this about async await. If the tasks aren't dependent on one another then this is the way to go.

dogyear
  • 288
  • 1
  • 7
7

Summary

The take home point here (and it's a very common misconception otherwise), is that await actually really does mean "await".


await operator (C# reference)

Emphasis mine

The await operator suspends evaluation of the enclosing async method until the asynchronous operation represented by its operand completes. When the asynchronous operation completes, the await operator returns the result of the operation, if any.

When the await operator is applied to the operand that represents an already completed operation, it returns the result of the operation immediately without suspension of the enclosing method.

The await operator doesn't block the thread that evaluates the async method. When the await operator suspends the enclosing async method, the control returns to the caller of the method.


So here is what is happening. In your first example you are starting each task and waiting for them complete consecutively. That's to say, it's like asking someone to go and do something and finish, before asking the next person to do something, etc

await TaskOne();    // start, do something and wait for it
await TaskTwo();    // start, do something and wait for it
await TaskThree();  // start, do  something and wait for it

Your second example. You are essentially starting the 3 tasks (hot), and then waiting for them to finish one at at time. That's to say they run concurrently, and awaited in sequence.

I.e. you are saying to 3 friends, go do stuff, then waiting for the first one to get back, then the second then the third. It's much more efficient... No pesky friends hanging around until the previous one gets back.

Even if the second task completes before the first, you are still in effect waiting for the first task before looking at the completed state of the second task etc.

var taskOne = TaskOne();     // start, do something
var taskTwo = TaskTwo();     // start, do something
var taskThree = TaskThree(); // start, do something

// all 3 tasks are started

await taskOne;   // wait for it
await taskTwo;   // wait for it
await taskThree; // wait for it

Or similarly you could use Task.WhenAll

Creates a task that will complete when all of the supplied tasks have completed.

var taskOne = TaskOne();     // start, do something
var taskTwo = TaskTwo();     // start, do something
var taskThree = TaskThree(); // start, do something

// wait for them all to finish!
await Task.WhenAll(taskOne, taskTwo, taskThree);
halfer
  • 19,824
  • 17
  • 99
  • 186
TheGeneral
  • 79,002
  • 9
  • 103
  • 141