0

Ok, I guess I'm almost there on understanding all the fuzz on async / await and multithreading. I understand that asynchronous is about tasks and multithreading is about workers. So you can have different tasks running on the same thread (this answer does a great job explaining it).

So I made a small programs in order to see the magic happening and I got a little confused:

public class Program
{
    public static async Task Main(string[] args)
    {
        var task = ToastAsync();
        Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Cleaning the kitchen...");
        await task;
        Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Take the toast");
    }

    public async static Task ToastAsync()
    {
        Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Putting the toast");
        Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Setting a timer");
        await Task.Delay(2000);
        Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Toast is ready");
    }
}

Before running this program for the first time, I expected it to run on a single thread. Like the answer I linked above, I expected that this was the equivalent of "cleaning the kitchen while the toast's timer is running". The results though contradict it:

[1] Putting the toast
[1] Setting a timer
[1] Cleaning the kitchen...
[4] Toast is ready
[4] Take the toast

The above result doesn't make much sense to me. What is really happening? It seems part of the function is being executed in one thread and then, when it hits an await point it handles the execution to another thread...? I didn't even know this was possible D:

Furthermore, I changed the above example a bit. In the main function, instead of await task; I used task.Wait(). And then the results changed:

[1] Putting the toast
[1] Setting a timer
[1] Cleaning the kitchen...
[4] Toast is ready
[1] Take the toast

Now this looks more like the example. It is like the timer on the toast worked as a different "cooker" though. But why is it different from using await? And is there a way of getting asynchronous task fully in a single thread? I mean, by having Toast is ready on thread 1 as well?

ThanksAsync!

João Menighin
  • 3,083
  • 6
  • 38
  • 80
  • Do you actually need it to run on one thread? Tasks are kind of an abstraction over threads. – JamesFaix Apr 12 '19 at 03:57
  • Also, avoid `.Wait()` whenever you can, it will block the current thread until the task finishes, which defeats the purpose of `async`. – JamesFaix Apr 12 '19 at 03:58

2 Answers2

5

The things to note are

  1. When you call ToastAsync it starts the Task Hot (even though you haven't called await). Note : Starting a Task does not mean starting a thread... Which in-turn explains why "Cleaning the kitchen." is behind "Putting the toast")

  2. The async method will run until hitting its first await keyword, and yields control back to the caller.

  3. Since there is no SynchronizationContext in a Console App. The Compiler sees no need to create a Continuation on the calling thread, which in-turn explains why "Take the toast" is on the same thread as "Toast is ready".

Note : If you clean the kitchen after you make a mess, you don't have to clean it beforehand

From comments

I understand then that an await will always take a thread from thread pool to run the the method (this is really blowing my mind) therefore it is basically impossible to have an async / await single threaded

The Async and Await pattern isn't about multi-threading per-se, it's about scalability and IO bound workloads, and/or UI responsiveness (in UI frameworks).

Take a look at some of Stephen Cleary's articles on the subject:

Async and Await - Stephen Cleary

halfer
  • 19,824
  • 17
  • 99
  • 186
TheGeneral
  • 79,002
  • 9
  • 103
  • 141
  • Hi Michael, thanks! Two things: First, On your first statement you mean the other way around right? "Putting the toast is behind cleaning the kitchen". Second, I understand then that an `await` will always take a thread from thread pool to run the the method (this is really blowing my mind) therefore it is basically impossible to have an `async / await` single threaded...? – João Menighin Apr 12 '19 at 02:24
  • @JoãoMenighin when you call `var task = ToastAsync()`, it starts the task (not a thread), it will run the first part of `ToastAsync` in the *context* its called form until it hits the first `await` this is why "Cleaning the kitchen..." is logged after "Putting the toast" – TheGeneral Apr 12 '19 at 02:26
  • Ah ok, sorry that was an english problem... Mistook "behind" by "before". Yep, that's clear. What about the "async await single-threaded"? – João Menighin Apr 12 '19 at 02:29
  • And actually, why using `task.Wait()` changes it anyway..? Any ideas? – João Menighin Apr 12 '19 at 02:33
  • @JoãoMenighin, it depends how fragile we need to get with our definitions here. The *Asnyc and Await pattern* isn't about multi-threading per-se, Its a about scalability and *IO bound* workloads. All the code above runs sequentially and linearly, there really isn't any multi-threading here. – TheGeneral Apr 12 '19 at 02:34
  • 4
    @JoãoMenighin forget about `Wait()` or `Result()` in the *Async and Await Pattern*, if you find your self wanting to use them you are probably doing something wrong – TheGeneral Apr 12 '19 at 02:35
  • @JoãoMenighin `task.Wait()` blocks the current thread until the task is completed. On the other hand `await task` doesn't block the thread. Instead it registers a callback (also called continuation) to run on the completion of the task. The callback contains the rest of the code inside `Main`. – Theodor Zoulias Apr 12 '19 at 02:52
  • 1
    @MichaelRandall great answer, as usual! However, I think this part: _"The Asnyc and Await pattern isn't about multi-threading per-se, Its a about scalability and IO bound workloads"_ is worthy to be a part of answer too. When you stop thinking of `async/await` as `multithreading` - you start to see the truth. – vasily.sib Apr 12 '19 at 03:28
0

If you run the same code from an event of a Windows Form you'll get the expected results:

[1] Putting the toast
[1] Setting a timer
[1] Cleaning the kitchen...
[1] Toast is ready
[1] Take the toast

This is because the await by default restores the context after the awaiting, but this has a cost because it's an extra thing to do. If you don't need the context restoration you can configure the await like this:

await Task.Delay(2000).ConfigureAwait(false);

...and you'll see the same output as the Console Application.

Keep in mind that awaiting is not about doing things concurrently, it is about keeping the UI responsive. You want to be able to keep receiving new orders for toasts, while the toasts from previous orders are cooking.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104