2

I'm trying to understand C# async/await and observed this confusing behavior where async methods don't execute past Task.Delay calls.

Consider the below -

class Program
{
    static void Main(string[] args)
    {
        Program p = new Program();
        p.MakeBreakfast();
    }

    public async Task MakeBreakfast()
    {
        await BoilWater();
        StartToaster();
        PutTeainWater();
        PutBreadinToaster();
        SpreadButter();
    }

    public async Task BoilWater() { Console.WriteLine("BoilWater start"); await Task.Delay(30); Console.WriteLine("BoilWater end"); }
    public async Task StartToaster() { Console.WriteLine("StartToaster start"); await Task.Delay(1); Console.WriteLine("StartToaster end"); }
    public async Task PutBreadinToaster() { Console.WriteLine("PutBreadinToaster start"); await Task.Delay(2000); Console.WriteLine("PutBreadinToaster end"); }
    public async Task PutTeainWater() { Console.WriteLine("PutTeainWater start"); await Task.Delay(30); Console.WriteLine("PutTeainWater end"); }
    public async Task SpreadButter() { Console.WriteLine("SpreadButter start"); await Task.Delay(10); Console.WriteLine("SpreadButter end"); }
}

Its output will be -

Boilwater Start
Press any key to continue...

What happened to "Boilwater end" statement and all other method calls? If I put async/await only in the BoilWater method, I get the same output.

If I remove await from all method calls -

    public async Task MakeBreakfast()
    {
         BoilWater();
         StartToaster();
         PutTeainWater();
         PutBreadinToaster();
         SpreadButter();
    }
Now, the output is - 
BoilWater start
StartToaster start
PutTeainWater start
PutBreadinToaster start
SpreadButter start
Press any key to continue . . .

Now, what happened to "end" statements? What's going on with async await in these examples?

Achilles
  • 1,099
  • 12
  • 29
  • 4
    Your program quits before `MakeBreakfast` had chance to complete. You should either `await` or return an `async` value, otherwise the completeness is not guaranteed. – zerkms Apr 27 '19 at 03:08
  • @Achilles, can you post the exact code you are running. There's something not quite right. To get the "Press any key to continue . . ." printed, you'll need a Console.Readline(). If you put that in, you'll get the output you expect. If you don't put that in, the program will exit before some of the Console.Writelines() have a chance to execute. – Phillip Ngan Apr 27 '19 at 05:40

3 Answers3

5

You program starts with the call to Main and it exits when it is done.

Since Main just creates an instance of Program and then calls MakeBreakfast(), which returns a Task back to main as soon as it hits its first await. Thus Main exists almost immediately.

Let's change the code slightly to see if this is the case:

static void Main(string[] args)
{
    Program p = new Program();
    p.MakeBreakfast();
    Console.WriteLine("Done!");
    Console.ReadLine();
}

public async Task MakeBreakfast()
{
    Console.WriteLine("Starting MakeBreakfast");
    Thread.Sleep(1000);
    Console.WriteLine("Calling await BoilWater()");
    await BoilWater();
    Console.WriteLine("Done await BoilWater()");
    StartToaster();
    PutTeainWater();
    PutBreadinToaster();
    SpreadButter();
}

Now if I let this run to completion I see this output:

Starting MakeBreakfast
Calling await BoilWater()
BoilWater start
Done!
BoilWater end
Done await BoilWater()
StartToaster start
PutTeainWater start
StartToaster end
PutBreadinToaster start
SpreadButter start
SpreadButter end
PutTeainWater end
PutBreadinToaster end

The code does indeed hit the await and then return to Main.

To make the code complete correctly we need to await everything. You have two ways you can do this:

(1)

static async Task Main(string[] args)
{
    Program p = new Program();
    await p.MakeBreakfast();
    Console.WriteLine("Done!");
    Console.ReadLine();
}

public async Task MakeBreakfast()
{
    await BoilWater();
    await StartToaster();
    await PutTeainWater();
    await PutBreadinToaster();
    await SpreadButter();
}

Now when this is run you get this output:

BoilWater start
BoilWater end
StartToaster start
StartToaster end
PutTeainWater start
PutTeainWater end
PutBreadinToaster start
PutBreadinToaster end
SpreadButter start
SpreadButter end
Done!

(2)

static async Task Main(string[] args)
{
    Program p = new Program();
    await p.MakeBreakfast();
    Console.WriteLine("Done!");
    Console.ReadLine();
}

public async Task MakeBreakfast()
{
    var tasks = new[]
    {
        BoilWater(),
        StartToaster(),
        PutTeainWater(),
        PutBreadinToaster(),
        SpreadButter(),
    };
    await Task.WhenAll(tasks);
}

Now this version starts all of the breakfast tasks at the same time but waits for them all to finish before returning.

You get this output:

BoilWater start
StartToaster start
PutTeainWater start
PutBreadinToaster start
SpreadButter start
StartToaster end
SpreadButter end
BoilWater end
PutTeainWater end
PutBreadinToaster end
Done!

An alternative which gives a more logical execution of the code - boil then water, then make the tea; and start toaster, cook toast, spread the toast - might be like this:

public async Task MakeBreakfast()
{
    async Task MakeTea()
    {
        await BoilWater();
        await PutTeainWater();      
    }

    async Task MakeToast()
    {
        await StartToaster();
        await PutBreadinToaster();
        await SpreadButter();           
    }       
    await Task.WhenAll(MakeTea(), MakeToast());
}

That gives:

BoilWater start
StartToaster start
StartToaster end
PutBreadinToaster start
BoilWater end
PutTeainWater start
PutTeainWater end
PutBreadinToaster end
SpreadButter start
SpreadButter end
Done!
Enigmativity
  • 113,464
  • 11
  • 89
  • 172
3

The general workflow of an async method is that the code until an await is run synchronously (i.e. as is), then a task object is returned with the task to be awaited and all the stuff after the await is put as a continuation of this task which is to be executed when the task completes.

Now, if only BoilWater is awaited the start message is executed synchronously and all the other calls are put as a continuation. As MakeBreakfast isn’t awaited the program will execute before BoilWater can complete/wait their milliseconds, and thus the continuations (i.e. other tasks) are not executed.

If BoilWater isn’t awaited, the other MakeBreakfast tasks are not put as a continuation of the BoilWater task. This means that BoilWater again runs until the Task.Delay and returns this as a task. However, as this task isn’t awaited, the next MakeBreakfast is started in the same manner. So essentially, all MakeBreakfast tasks are started sequentially and MakeBreakfast can only return when SpreadWater is started and returns it’s task. Again, the tasks still run in the background waiting their milliseconds but the program exits before this timeframe and thus the closing message continuations have no chance of being run.

ckuri
  • 3,784
  • 2
  • 15
  • 17
  • "all the stuff after the await is put as a continuation of this task which is to be executed when the task completes" - I don't believe there are task continuations. It's all about state machines. Have a look at `System.Runtime.CompilerServices.AsyncTaskMethodBuilder`. – Enigmativity Apr 27 '19 at 05:31
  • @Enigmativity True, the after-`await` stuff is not a real continuation like `ContinueWith` as the latter is much more simplified (e.g. doesn't consider resuming on the same synchronisation context) and thus has no need for an elaborate state machine. But in general (and especially when there is no synchronisation context like on a console), `await task; DoStuff(); return;` follows roughly the same workflow as `return task.ContinueWith(t => DoStuff(), TaskContinuationOptions.OnlyOnRanToCompletion);`. I was talking from a workflow perspective, and not exactly from an implementation perspective. – ckuri Apr 27 '19 at 05:45
-2

What happened to "Boilwater end" statement and all other method calls? If I put async/await only in the BoilWater method, I get the same output.

As was zerkms said, your program exits before your task is complete.

If I remove await from all method calls -

I believe if await is not called anywhere within an async Task, then it just processes it synchronously; this would explain why the output shows all the "start" messages.

As for what happened to the "end" statements, I believe that if you do not await Task.Delay, then it wont actually wait (delay) and your code will continue. Actually I just found this which explains it better.

Buretto
  • 405
  • 4
  • 12