---
title: "async2 - The .NET Runtime Async experiment concludes"
description: "The .NET team has been working on a new experiment called async2, which is a new implementation of the async/await pattern that is designed to be more efficient and more flexible than the current implementation. It started with green threads and ended with an experiment that moves `async` and `await` to the runtime. This post will cover the journey of `async2` and the conclusion of the experiment."
updated: 2024-08-12
tags: ["C#", "async", "await", ".net"]
canonical: "https://steven-giesel.com/blogPost/59752c38-9c99-4641-9853-9cfa97bb2d29/async2-the-net-runtime-async-experiment-concludes"
---

# async2 - The .NET Runtime Async experiment concludes

The .NET team has been working on a new experiment called async2, which is a new implementation of the async/await pattern that is designed to be more efficient and more flexible than the current implementation. It started with green threads and ended with an experiment that moves `async` and `await` to the runtime. This post will cover the journey of `async2` and the conclusion of the experiment.

## Where all began - Green threads

Let's start here: [*"Green Thread Experiment Results"*](https://github.com/dotnet/runtimelab/issues/2398). The team invested in an experiment to evaluate the feasibility of using green threads in the .NET runtime. But wait a second, what are green threads?

### Green threads
Green threads are user-space threads that are managed by a runtime library or a virtual machine (VM) instead of the operating system.<sup>Source: [Wikipedia](https://en.wikipedia.org/wiki/Green_thread)</sup> They are lightweight and can be created and managed more quickly than kernel threads. Green threads are also known as "coroutines" or "fibers" in other programming languages. The idea is that you, as a developer, don't have to worry about threads.

Currently, with threads and to some extend with `async`/`await`, a new stack is created. You can easily see that in your favourite IDE, if you debug:

![stack](https://linkdotnetblog.azureedge.net/blog/20240812_Async2/stacks.webp)

Green threads are different. [The memory of a green thread is allocated on the heap](https://stackoverflow.com/a/19098856/1892523). But all of this comes with a cost: As they aren't managed by the OS, they can't take advantage of multiple cores inherently. But for I/O-bound operations, they are a good fit.

### Abandoning green threads
The key-challenges were (which lead to the abandonment of the green threads experiment):
 * Complex interaction between green threads and existing async model
 * Interop with native code was complex and slower than using regular threads
 * Compatibility issues with security mitigations like shadow stacks
 * Uncertainty about whether it would be possible to make green threads faster than async in important scenarios, given the effort required for improvement.

This lead to the conclusion that green threads are not the right way to go for the .NET runtime and gave birth to the `async2` experiment. From here on out, I will keep the term `async2` for the experiment, as it is the codename for the experiment.

## `async2` - The .NET Runtime Async experiment
Now, `async2` is obviously only a codename. The goal of the experiment was to move `async` and `await` to the runtime. The main motivation behind this was to make `async` more efficient and more flexible. As `async` is already used as an identifier in C#, the team decided to use `async2` as a codename for the experiment. **If** that thing ever makes it into the runtime, it will be called `async` - so it will be a replacement for the current `async` implementation. But let's start at the beginning.

### `async` is a compiler feature
I talked about this from time to time in my blog posts. For example:
 * [*"The state machine in C# with async/await"*](https://steven-giesel.com/blogPost/720a48fd-0abe-4c32-83ac-26926d501895/the-state-machine-in-c-with-asyncawait)
 * [*"C# Lowering"*](https://steven-giesel.com/blogPost/69dc05d1-9c8a-4002-9d0a-faf4d2375bce/c-lowering)

The current implementation of `async` and `await` is a compiler feature. The compiler generates a state machine for the `async` method. The runtime doesn't know anything about `async` and `await`. There is no trace of an `async`-like keyword in IL or in the JIT-compiled code. And that is where the experiment started.

Starting point is this nice GitHub issue: ["*.NET 9 Runtime Async Experiment*"](https://github.com/dotnet/runtime/issues/94620), which basically describes the whole experiment in more detail with an ongoing discussion from the community.

### `async` is a runtime feature
The goal of the experiment was to move `async` and `await` to the runtime. This would allow the runtime to have more control over the pattern itself. With that there would be also some different semantics:

### `async2` and `ExecutionContext` and `SynchronizationContext`

`async2` would not have save or restore of `SynchronizationContext` and `ExecutionContext` at function boundaries, instead allowing callers to observe changes. With the `ExecutionContext`, this would shift a big change in how `AsyncLocal` behaves.

Today, `AsyncLocal` is used to store data that flows with the logical call context. It gets copied to the new context. That said, if a function deep down the call stack changes the value of an `AsyncLocal`, the caller will **not** see the updated value, only functions further down the logical async flow. Here an example:

```csharp
await new AsyncLocalTest().DoOuter();

public class AsyncLocalTest
{
    private readonly AsyncLocal<string> _asyncLocal = new();

    public async Task DoOuter()
    {
        _asyncLocal.Value = "Outer";
        Console.WriteLine($"DoOuter: {_asyncLocal.Value}");
        await DoInner();
        Console.WriteLine($"DoOuter: {_asyncLocal.Value}");
    }

    private async Task DoInner()
    {
        _asyncLocal.Value = "Inner";
        Console.WriteLine($"DoInner: {_asyncLocal.Value}");
        await Task.Yield();
        Console.WriteLine($"DoInner: {_asyncLocal.Value}");
    }
}
```

The output of this code is:

```
DoOuter: Outer
DoInner: Inner
DoInner: Inner
DoOuter: Outer
```

With `async2` those changes are not "reverted" which would lead to a different output:

```
DoOuter: Outer
DoInner: Inner
DoInner: Inner
DoOuter: Inner
```

### Comparison with the current implementation and some results

The whole document, that describes all the details, can be found here: https://github.com/dotnet/runtimelab/blob/feature/async2-experiment/docs/design/features/runtime-handled-tasks.md

The team found out that the approach of putting `async` into the JIT might yield the best results overall. Here a basic overview:

| Feature | `async` | `async2` |
|---------|---------|----------|
| Performance | Generally slower than `async2`, especially for deep call stacks | Generally faster than `async`, with performance comparable to synchronous code in non-suspended scenarios |
| Exception Handling | Slow and inefficient, causing GC pauses and impacting responsive performance of applications | Improved EH handling, reducing the impact on application responsiveness |
| Stack Depth Limitation | Limited by stack depth, which can cause issues for deep call stacks | No explicit limitations on stack depth, allowing `async2` to handle deeper call stacks more efficiently |
| Memory Consumption | Generally lower than `async2`, especially in scenarios with many suspended tasks | Higher memory consumption due to capturing entire stack frames and registers, but still acceptable compared to other factors like pause times |


## Where do we go from here?
As the name suggests, this is just an experiment, that may lead to a replacement of `async` **in some years**. Yes, **years**. It might take a while until this is production-ready. And for the transition phase, there has to be interop for `async` <-> `async2`. Anyway - a very good starting point and I am looking forward to the future of `async2`.