Visual Guide to C# async/await
Tags: csharp, asynchronous, async, dotnet
Published: 2021-07-06 16:04:25

---
title: Visual Guide to C# async/await
published: true
date: 2021-07-04 22:02:27 UTC
tags: csharp,asynchronous,async,dotnet
canonical_url: https://medium.com/@joni2nja/visual-guide-to-csharp-async-await-bc85db77f36a
cover_image: https://cdn-images-1.medium.com/max/1024/1*H5AMh4HPJDHfMxeb324j8A.png
---

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_](https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/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](https://blog.stephencleary.com/2013/11/there-is-no-thread.html)” 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#](https://cdn-images-1.medium.com/max/700/1*36pNjD8qAjiUhkyidq-Z7Q.gif)<figcaption>Visualized: Ordinary, synchronous method call in C#</figcaption>

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

### Asynchronous code

Next, the asynchronous one.

![Visualized: Asynchronous method call in C#](https://cdn-images-1.medium.com/max/700/1*JUlC43DA7pMGtaRt2Sg_eg.gif)<figcaption>Visualized: Asynchronous method call in C#</figcaption>

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](https://docs.microsoft.com/en-us/dotnet/api/system.threading.synchronizationcontext)” 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](https://docs.microsoft.com/en-us/dotnet/api/system.threading.synchronizationcontext)), 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](https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-4/#introducing-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();
```

### Synchronous code

First, we will inspect the synchronous version. Go to _https://localhost:5001/sleep_. Inspect the process using [Process Explorer](https://docs.microsoft.com/en-us/sysinternals/downloads/process-explorer).

![Inspect using Process Explorer](https://cdn-images-1.medium.com/max/839/1*f513CGWiuYjqzDPP_SwmvQ.png)<figcaption>Inspect using Process Explorer</figcaption>

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](https://cdn-images-1.medium.com/max/551/1*GtN_HrYHeJE48E_RpzGvLg.png)<figcaption>Break All in Visual Studio</figcaption>

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

Look at the **Parallel Stacks**  window.

![Parallel Stacks window](https://cdn-images-1.medium.com/max/1024/1*GKDJa4Pxf9-G-MmLnLwMXg.png)<figcaption>Parallel Stacks window</figcaption>

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.](https://cdn-images-1.medium.com/max/775/1*7WRfuChV1IAaDgGf42rpPg.png)<figcaption>View by Threads.</figcaption>

Let’s see **Threads** window. Notice the scrollbar?

![Threads window](https://cdn-images-1.medium.com/max/1024/1*mUajcQvatkDFqzZ4-srh5Q.png)<figcaption>Threads window</figcaption>

### Asynchronous code

Next, let’s see asynchronous version. Go to _https://localhost:5001/delay_. Inspect the process using [Process Explorer](https://docs.microsoft.com/en-us/sysinternals/downloads/process-explorer).

![Inspect using Process Explorer](https://cdn-images-1.medium.com/max/841/1*VoliFRL3PX6NEyRpHZokOQ.png)<figcaption>Inspect using Process Explorer</figcaption>

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](https://cdn-images-1.medium.com/max/551/1*GtN_HrYHeJE48E_RpzGvLg.png)<figcaption>Break All in Visual Studio</figcaption>

Look at the **Parallel Stacks**  window.

![Parallel Stacks window. A bunch of tasks are in “scheduled” state.](https://cdn-images-1.medium.com/max/1024/1*MhgVpD6T9vJ8ss-wfohXPw.png)<figcaption>Parallel Stacks window. A bunch of tasks are in “scheduled” state.</figcaption>

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.](https://cdn-images-1.medium.com/max/928/1*a8GVw2Dzsjs68_DwWqqhEg.png)<figcaption>View by Threads.</figcaption>

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

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

![Threads window](https://cdn-images-1.medium.com/max/1024/1*Ji0Yk4aOeclAJIXiusefGw.png)<figcaption>Threads window</figcaption>

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.