0

I clearly understand TAP model execution in C#. However, when it comes to concurrent tasks I get confused.

Consider this example in MS docs:

Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");

Task<Egg> eggsTask = FryEggsAsync(2);
Task<Bacon> baconTask = FryBaconAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);

Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");

Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");

Console.WriteLine("Breakfast is ready!");

Calling async without awaiting it's just very confusing.

Let's take this line for example: Task<Egg> eggsTask = FryEggsAsync(2);

1- Main thread will call the function and start executing it.

2- When the main thread counters the first await it will return to the Main() function.

Now, when the await Task.Delay(3000); in the FryEggsAsync() completes what is happening?

private static async Task<Egg> FryEggsAsync(int howMany)
{
    Console.WriteLine("Warming the egg pan...");
    await Task.Delay(3000);
    Console.WriteLine($"cracking {howMany} eggs");
    Console.WriteLine("cooking the eggs ...");
    await Task.Delay(3000);
    Console.WriteLine("Put eggs on plate");

    return new Egg();
}

I tried to debug this example with Rider and I got that when the await Task.Delay(3000); completes FryEggsAsync() spawn thread from the thread pool and continue execution with it.

Does that mean that each of the three functions will continue the execution in the background with threads from the pool without the need to continue it on the main thread?

Or the function is suspended at the await line and doesn't proceed until I await it from the Main() function?

2 Answers2

1

Now, when the await Task.Delay(3000); in the FryEggsAsync() completes what is happening?

The continuation is invoked (i.e. everything after the the await Task.Delay(3000)). In "simple"/default case of console application it will be invoked on the thread pool, but there are some caveats when SynchronizationContext is present.

Does that mean that each of the three functions will continue the execution in the background with threads from the pool without the need to continue it on the main thread?

In case of the sample console app - yes.

Or the function is suspended at the await line and doesn't proceed until I await it from the Main() function?

No. await in the Main is needed to return the thread control which executes Main in case Main needs to wait for the finish of the executed task.

Read more:

  1. Asynchronous Programming in .NET - Introduction, Misconceptions, and Problems - great post about the topic
  2. There Is No Thread by async guru Stephen Cleary (read his other articles too)
  3. What does SynchronizationContext do?
  4. async/await deadlocking when using a SynchronizationContext
  5. Dissecting the async methods in C# - deep dive into async including description of compiler generated state machine
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
1

Task does not in itself say anything about concurrency. It just represents an operation, possibly with a result. It may already be completed, it may complete in the future, or it may never complete. It does not really say anything about threads. In many cases it will represent an asynchronous IO operation that never uses any thread to run at all.

await essentially says, when the task completes, continue execution. And do so in the same 'context' as you started in. I.e. the UI thread if called from the UI thread, or some threadpool thread if called from the threadpool. Note that console programs do not have any UI thread, so the only context is the threadpool context.

Task.Delay(3000); is essentially just a wrapper around Threading.Timer. So after the delay the OS will send a message to the threadpool, please run this code on some available thread. The threadpool will do so, that code will complete the task. What happens next will depend on the captured context of each awaiter.

Does that mean that each of the three functions will continue the execution in the background with threads from the pool without the need to continue it on the main thread?

Since the example uses a console program there is no UI thread. There is a main thread, but it still uses the thread pool context. So anything after an await will likely be invoked on a threadpool thread. This works since it uses Task Main, i.e. the program will terminate once the main task completes.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
JonasH
  • 28,608
  • 2
  • 10
  • 23
  • Oh. I get it. But let's say I have a GUI app, maybe WinForms, according to my understanding; We have GUI main thread Therefore there will be SynchronizationContext in this case, and continuation will need to execute on the thread that calls it. Correct me if there are any mistakes – Mohab Alnajjar Mar 01 '23 at 10:10
  • @MohabAlnajjar Yes, if you call an async method on the *UI* thread, then continuations will also run on the UI thread. The simplest case is if you have a UI program and only await third party functions that return tasks, then all of *your code* should run on the UI thread, and you don't have to worry that much about threading issues. – JonasH Mar 01 '23 at 10:24