0

Why doesn't one await statement make all other statements in the same method async-await calls?

From MS docs:

static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
    var toast = await ToastBreadAsync(number);
    ApplyButter(toast);
    ApplyJam(toast);

    return toast;
}

On the page it says,

Composition with tasks

You have everything ready for breakfast at the same time except the toast. Making the toast is the composition of an asynchronous operation (toasting the bread), and synchronous operations (adding the butter and the jam). Updating this code illustrates an important concept:

Important

The composition of an asynchronous operation followed by synchronous work is an asynchronous operation. Stated another way, if any portion of an operation is asynchronous, the entire operation is asynchronous.

Later on in the final version, one of the methods shows two await statements. Why? If the method has the first await on Task.Delay, why do I need another await on the second Task.Delay? Wouldn't the Task.Delay make this async thread "await" here anyway?

private static async Task<Bacon> FryBaconAsync(int slices)
        {
            Console.WriteLine($"putting {slices} slices of bacon in the pan");
            Console.WriteLine("cooking first side of bacon...");
            await Task.Delay(3000);
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("flipping a slice of bacon");
            }
            Console.WriteLine("cooking the second side of bacon...");
            await Task.Delay(3000);
            Console.WriteLine("Put bacon on plate");

            return new Bacon();
        }

This did not tell me the why: Why do we need more than one `await` statement in a C# method?

Nor this,

C# Async Await with Synchronous and Asynchronous operation

learntofix
  • 87
  • 5
  • 3
    If you do not `await` something that is `async` then you are "firing and forgetting" meaning you are not waiting for the task to complete. That means your method can complete before the Task you didn't await. Awaiting one thing in a method does not automatically make it await everything else. – juharr Jul 26 '22 at 16:59
  • If I took out the second await here, what would happen then? – learntofix Jul 26 '22 at 17:09
  • 3
    `this async thread` - there is [no such thread](https://stackoverflow.com/q/17661428/11683). I take it when you say "make async" you mean "start running this on a separate thread"; this is not what happens. `If I took out the second await here, what would happen then?` - then "Put bacon on plate" would appear immediately after "cooking the second side of bacon...", rather than three seconds later. – GSerg Jul 26 '22 at 17:09
  • @GSerg yes, that is what I thought was happening. – learntofix Jul 26 '22 at 17:10
  • 1
    @learntofix Click the link then. Also https://stackoverflow.com/q/34680985/11683, https://stackoverflow.com/q/37419572/11683 and https://blog.stephencleary.com/2013/11/there-is-no-thread.html. – GSerg Jul 26 '22 at 17:12
  • 1
    *"Why do I need another `await` on the second `Task.Delay`?"* -- The first `await Task.Delay` represents the cooking of the bacon's first side. The second `await Task.Delay` represents the cooking of the bacon's second side. So your question, as I understand it, is *"why do we need to cook the bacon from both sides?"* Which is a peculiar question to ask. If you are satisfied with a half-baked bacon, nobody can argue against that. It's none of our business to tell you how to eat your bacon! – Theodor Zoulias Jul 26 '22 at 17:22
  • @TheodorZoulias lol, but my point is the delay would happen regardless, I thought, so why do I need the method to stop there? – learntofix Jul 26 '22 at 17:38
  • I still don't understand. Do you think that the `Task.Delay(3000)` is equivalent with the `await Task.Delay(3000)`, and in both cases the method will be suspended at that point for 3 seconds? Or you believe that suspending the method is not needed, because baking the bacon's second side can happen instantaneously? – Theodor Zoulias Jul 26 '22 at 17:46
  • The reason "Why doesn't one await statement make all other statements in the same method async-await calls?" is because that's not how the C# designers designed this part of the language. If you want to know what went through their head when they decided on this, only they can answer that. I would imagine the main reason would be that you can then grab the tasks you want to let finish in parallel and await them after spawning all of them, the choice is then yours. – Lasse V. Karlsen Jul 26 '22 at 17:49
  • I think I simply don't understand the definition. – learntofix Jul 26 '22 at 20:00
  • @TheodorZoulias when in that async method when it hits await, is that method "stopped" until that await is done? Nothing else runs until that await is done in that method (synchronous)? – learntofix Jul 26 '22 at 20:09
  • Yes, the `await` suspends the asynchronous method, until the awaitable completes. Other things might be happening concurrently elsewhere, but the method (or using another term "the execution flow") is suspended. – Theodor Zoulias Jul 26 '22 at 20:30

2 Answers2

1

Task.Delay doesn't delay your current task. It creates another task that waits for that amount. So if you just do Task.Delay(123) you are spawning another task that waits for 123 ms, but your current task doesn't wait for that spawned task to finish. In order to make your current task wait for 123 ms as well, you need to await Task.Delay(123), which makes the current task wait for the spawned task, which waits for 123 ms. Because await pauses the current task's execution at that point to wait for the given other task to complete.


Note: Please note the comment by Theodor Zoulias:

It's better to say that the Task.Delay creates a task that will complete after that amount of time. There is no waiting taking place internally.

starikcetin
  • 1,391
  • 1
  • 16
  • 24
  • 2
    *"It creates another task that waits for that amount."* -- I am afraid that using the term "waits" to explain the `Task.Delay` method, can result in confusion. IMHO it's better to say that the `Task.Delay` creates a task that will *complete after* that amount of time. There is no waiting taking place internally. If you create 1,000,000 `Task.Delay` tasks, you are not creating 1,000,000 [waiters](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.taskawaiter). – Theodor Zoulias Jul 26 '22 at 17:54
1

awaits are there so that the next statement executes after the work is done.

Let's imagine a piece of code that requires a sequence of steps.

async Task<bool> ProcessAsync() 
{
   var data = await GetDataAsync();
   await SendDataOrThrowAsync(data);
   return true; // Success
}

The 1st await is easy - you cannot send data before you get it. The 2nd await is there so that return true is executed only when the sending is finished.

Without the 2nd await the contract changes.

async Task<bool> ProcessAsync() 
{
   var data = await GetDataAsync();
   SendDataOrThrowAsync(toast); // don't wait just start
   return true; // Success
}

A true here means that we started sending not that we sent it.


I think this is also a good example that I hope helps clearing the confusion.

With

await Task.Delay(1000);
await Task.Delay(2000);

DoWork();

DoWork will be called after 3 seconds.

With

await Task.Delay(1000); // Start, and pause execution here until done
Task.Delay(2000); // Start, but don't pause;

DoWork();

DoWork will be called after 1 second.

Having the ability to to await or not to await allows you to do multiple things at the same time.

async Task<int> GetFirstHalfAsync() => await Task.Delay(1000);
async Task<int> GetSecondHalfAsync() => await Task.Delay(3000);

(...)

var t1 = GetFirstHalfAsync(); // Start, don't pause here
var t2 = GetFirstHalfAsync(); // Start, don't pause here

var results = await Task.WhenAll(new Task<int>[] { t1, t2 }); // Pause here until we get the results 
tymtam
  • 31,798
  • 8
  • 86
  • 126