https://steven-giesel.com/blogPost/59752c38-9c99-4641-9853-9cfa97bb2d29 brand * Home * Archive * Portfolio * About me * RSS * Log in * [ ] async2 - The .NET Runtime Async experiment concludes 12/08/2024 C#asyncawait.net Table of Contents Where all began - Green threadsGreen threadsAbandoning green threads async2 - The .NET Runtime Async experimentasync is a compiler feature async is a runtime featureasync2 and ExecutionContext and SynchronizationContextComparison with the current implementation and some resultsWhere do we go from here? 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". 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.^ Source: Wikipedia 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 Green threads are different. The memory of a green thread is allocated on the heap. 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" * "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", 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: await new AsyncLocalTest().DoOuter(); public class AsyncLocalTest { private readonly AsyncLocal _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 Generally slower than Generally faster than async, Performance async2, especially for with performance comparable to deep call stacks synchronous code in non-suspended scenarios Slow and inefficient, Exception causing GC pauses and Improved EH handling, reducing Handling impacting responsive the impact on application performance of responsiveness applications Limited by stack depth, No explicit limitations on stack Stack Depth which can cause issues depth, allowing async2 to handle Limitation for deep call stacks deeper call stacks more efficiently Generally lower than Higher memory consumption due to Memory async2, especially in capturing entire stack frames Consumption scenarios with many and registers, but still suspended tasks 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. 20 * Copy To Clipboard * ----------------------------------------------------------------- * Share on LinkedIn * Share on X Buy Me a Coffee at ko-fi.com Sponsors Become a patreon Want to read more? Check out these related blog posts! The state machine in C# with async/await You often here that the async/await keywords leads to a state machine. But what does that mean? Let's discuss this with a simple example. ASP.NET Core - Why async await is useful Did you ever wonder why you "should" use async and await in your ASP.NET Core applications? Most probably, you heard something about performance. And there is some truth to it, but not in the way you might think. So let's discuss this with smaller examples. Async Await Pitfalls / Guidelines - .NET User Group Zurich Presentation On 6th of July I had the honor to present some topics about async/ await. Mainly: * What is asynchronous programming * Deadlocks and ConfigureAwait * How does the state machine work * Pitfalls and general Guidelines * ValueTask You'll find all the slides and the whole talk in the blog. (c) 2024 Steven Giesel An error has occurred. This application may no longer respond until reloaded. Reload x