Visual Guide to C# async/await

joni2nja

Joni 【ジョニー】

Posted on July 6, 2021

Visual Guide to C# async/await

Microsoft first introduced async/await pattern in C# 5.0 back in 2011. I think it’s one of the greatest contributions to the asynchronous programming — at languages level— which led other programming languages to follow, like Python and JavaScript, to name a few. It makes asynchronous code more readable, more like the ordinary synchronous code. Remember those old-school BeginXxx / EndXxx in Asynchronous Programming Model/APM in C# 1.0? I can still remember writing those in 2002 with Visual Studio .NET 2002.

Enough foreword. Do you still remember the famous “There Is No Thread” post from Stephen Cleary?

If you’re newbie to C#, go read it. I will wait. Perhaps, I should say await ReadAsync() ?😆

Okay, I’m glad you’re still here. This post is my attempt to help C# developers to better grasp what async/await is all about.

Synchronous code

Let’s start with the synchronous version.

Visualized: Ordinary, synchronous method call in C#
Visualized: Ordinary, synchronous method call in C#

Nothing fancy here. I guess it’s pretty straightforward and self-explanatory.

Asynchronous code

Next, the asynchronous one.

Visualized: Asynchronous method call in C#
Visualized: Asynchronous method call in C#

As you can see, our asynchronous chef put await MethodAsync() in the kitchen and then leaves the kitchen without waiting for the 🍜(Task<🍜>) to be ready. Our synchronous chef, whereas, in contrast, will be hanging around, perhaps forever in the kitchen, waiting for the 🍜.

Our asynchronous chef leaves the kitchen, returns to his home (thread pool ), waiting with his lovely family members (thread pool threads ) while keeping the door open (assuming ConfigureAwait(false); trying to simulate “no Synchronization Context” here). At this point, we are in the so-called “there is no thread” state. Nobody is in the kitchen. Our 🍜 is still sitting in the microwave (ongoing I/O operation). Sorry to disappoint you, my dear reader, it’s instant noodles 😂.

Once it’s heated, our super smart AI-powered-alarm-shaped drone (I/O Completion Port — IOCP) flies to our chef’s house to notify them. Remember that the door is left open (again, no Synchronization Context), but our chef is in the toilet 🚽, so he asked his wife — who happened to be a chef as well — to go to continue his work. She resumes her husband’s remaining tasks, picks where her husband left it off (resuming AsyncStateMachine). The rest is the same as the synchronous version.

Visualize using Visual Studio

The animated GIFs are there to illustrate analogies in the our-almost-real-world. Let’s try to visualize it using Visual Studio.

We’ll be using the following code, a minimal APIs, new in .NET 6.

var builder = WebApplication.CreateBuilder(args);
await using var app = builder.Build();

if (app.Environment.IsDevelopment())
{
  app.UseDeveloperExceptionPage();
}

app.MapGet("/sleep", (CancellationToken cancellationToken) =>
{
  while (!cancellationToken.IsCancellationRequested)
  {
    Enumerable.Range(1, 100).Select(x => x).ToList().ForEach(x =>
    {
      //WARNING: BAD CODE
      Task.Run(() => Thread.Sleep(3 * 60 * 1_000), cancellationToken);
    });
    Thread.Sleep(2 * 60 * 1_000);
  }
  return "Done.";
});

app.MapGet("/delay", async (CancellationToken cancellationToken) =>
{
  while (!cancellationToken.IsCancellationRequested)
  {
    Enumerable.Range(1, 100).Select(x => x).ToList().ForEach(x =>
    {
      //WARNING: BAD CODE
      Task.Run(async () => await Task.Delay(3 * 60 * 1_000, cancellationToken), cancellationToken);
    });
    await Task.Delay(2 * 60 * 1_000, cancellationToken);
  }
  return "Done.";
});

await app.RunAsync();
Enter fullscreen mode Exit fullscreen mode

Synchronous code

First, we will inspect the synchronous version. Go to https://localhost:5001/sleep. Inspect the process using Process Explorer.

Inspect using Process Explorer
Inspect using Process Explorer

We can see that we are starting up 100 threads. Notice the scrollbar? We have a bunch of threads hanging around.

📝 Even though we are calling Thread.Sleep, it’s still a waste of resources.

Let’s go back to Visual Studio, use Break All to pause the application execution.

Break All in Visual Studio
Break All in Visual Studio

📝 You can use Debugger.Break to achieve the same effect. Details: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.debugger.break

Look at the Parallel Stacks window.

Parallel Stacks window
Parallel Stacks window

A lot of sleeping threads are in “blocked” state. It’s like our synchronous chef in the kitchen doing nothing except waiting for the 🍜 to be heated in the microwave.

Now, switch the View to Threads view to see how it looks, grouped by threads. Hundreds of threads!

View by Threads.
View by Threads.

Let’s see Threads window. Notice the scrollbar?

Threads window
Threads window

Asynchronous code

Next, let’s see asynchronous version. Go to https://localhost:5001/delay. Inspect the process using Process Explorer.

Inspect using Process Explorer
Inspect using Process Explorer

We have started 100 tasks, but hey, there is no scrollbar!

Go back to Visual Studio, pause the application execution.

Break All in Visual Studio
Break All in Visual Studio

Look at the Parallel Stacks window.

Parallel Stacks window. A bunch of tasks are in “scheduled” state.
Parallel Stacks window. A bunch of tasks are in “scheduled” state.

Lots of tasks are in a “scheduled” state; scheduled to be fired in the future. In Thread column, you can see that there is no thread info, no thread ID.

Did you just scroll up to double-check the screenshot of the synchronous version? Welcome back! 😆

Now, switch the View to Threads view to see how it looks, grouped by threads.

View by Threads.
View by Threads.

Less than ten threads, that’s mostly the framework threads.

Let’s see Threads window. Notice that there is no scrollbar.

Threads window
Threads window

So yes, there is “no thread” here. No user code thread, to be precise. No thread, no chef. Like our asynchronous chef, instead of waiting in the kitchen, he returns to his home sweet home.

That’s it!

So what does this mean? For ASP.NET, this “ no thread” will translate to a scalability improvement since we don’t block our precious thread pool threads; blocking might lead to thread pool starvation. Here is the analogy. We only have five chefs. All of them are now waiting in the kitchen, doing nothing. We can no longer process additional cooking orders. But if they don’t simply wait in the kitchen, they would be able to process the same amount of orders with, say, just only two chefs. Given that two busy chefs in the kitchen, we still have three remaining chefs idle, waiting for the new additional cooking orders.

💖 💪 🙅 🚩
joni2nja
Joni 【ジョニー】

Posted on July 6, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Visual Guide to C# async/await
csharp Visual Guide to C# async/await

July 6, 2021