Visual Guide to C# async/await
By: Joni, ジョニー
Tags: Asynchronous, Dotnet, Programming, Csharp, Async

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#

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

Asynchronous code

Next, the asynchronous one.

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();

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

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

📝 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.