DEV Community

Vikrant Bagal
Vikrant Bagal

Posted on

.NET 11 Runtime Async (V2): 50% Faster Async, Cleaner Stack Traces

TL;DR: .NET 11 Preview 2 introduces Runtime Async (V2), moving async state machine management from compiler to runtime. This delivers 50% better async throughput, 80% faster cold starts, and cleaner debugging experience - eliminating the dreaded async-related deadlocks that plague mixed sync/async code.

Introduction

If you've ever struggled with async deadlocks when mixing modern async/await with older synchronization primitives like locks or ManualResetEventSlim, .NET 11 Preview 2 has your back. The new Runtime Async (V2) feature fundamentally changes how async/await works under the hood, moving the complex state machine infrastructure from the compiler into the runtime itself.

Technical Deep Dive

The Problem with Compiler-Generated Async

Traditionally, C#'s async/await relies on the compiler generating complex state machine classes. Each async method creates:

  • A state machine struct implementing IAsyncStateMachine
  • Multiple fields for tracking state, locals, and awaiters
  • Complex MoveNext() methods with switch statements
  • Significant overhead in memory allocations and method calls

This approach works fine but creates challenges:

  • Debugging nightmares: Stack traces filled with compiler-generated infrastructure
  • Breakpoint issues: Debuggers struggle to bind correctly inside async methods
  • Deadlock risks: Mixing async code with synchronization primitives can cause elusive deadlocks
  • Performance overhead: Extra method calls and allocations for state machine management

Runtime Async (V2) Solution

.NET 11 Preview 2's Runtime Async (V2) moves this complexity into the runtime:

<!-- In your .csproj -->
<PropertyGroup>
  <Features>runtime-async=on</Features>
  <EnablePreviewFeatures>true</EnablePreviewFeatures>
</PropertyGroup>
Enter fullscreen mode Exit fullscreen mode

With this enabled, the compiler emits methods with MethodImplOptions.Async, signaling the runtime to handle async suspension/resumption directly.

Before (Compiler-generated): 13 frames in stack trace

$>g__InnerAsync|0_2() in Program.cs:line 24
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.$>g__InnerAsync|0_2()
at Program.$>g__MiddleAsync|0_1() in Program.cs:line 14
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.$>g__MiddleAsync|0_1()
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.$>g__OuterAsync|0_0() in Program.cs:line 8
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.$>g__OuterAsync|0_0()
at Program.$(String[] args) in Program.cs:line 3
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.$(String[] args)
at Program.(String[] args)
Enter fullscreen mode Exit fullscreen mode

After (Runtime Async): Clean 5 frames

$>g__InnerAsync|0_2() in Program.cs:line 24
at Program.$>g__MiddleAsync|0_1() in Program.cs:line 14
at Program.$>g__OuterAsync|0_0() in Program.cs:line 8
at Program.$(String[] args) in Program.cs:line 3
at Program.(String[] args)
Enter fullscreen mode Exit fullscreen mode

Performance Impact

The benefits aren't just cosmetic - they translate to real performance gains:

  • Async throughput: 150K requests per second (+50% improvement)
  • Cold start latency: Reduced to 35ms (-80% from previous versions)
  • Memory efficiency: 38% reduction in async-heavy scenarios
  • Interface dispatch: Up to 200x improvement on iOS/Android (no JIT)
  • GUID generation: ~12% faster on Linux via optimized syscall batching

Real Code Example

Here's how you enable and use Runtime Async (V2):

// YourProject.csproj
<PropertyGroup>
  <Features>runtime-async=on</Features>
  <EnablePreviewFeatures>true</EnablePreviewFeatures>
</PropertyGroup>

// Program.cs
using System.Diagnostics;
using System.Threading.Tasks;

await OuterAsync();

static async Task OuterAsync()
{
    await Task.CompletedTask;
    await MiddleAsync();
}

static async Task MiddleAsync()
{
    await Task.CompletedTask;
    await InnerAsync();
}

static async Task InnerAsync()
{
    await Task.CompletedTask;
    // Clean stack trace shows real method names
    Console.WriteLine(new StackTrace(fNeedFileInfo: true));
}
Enter fullscreen mode Exit fullscreen mode

Migration Path

Adopting Runtime Async (V2) is straightforward:

  1. Enable preview features in your project file
  2. Add the runtime-async feature flag
  3. Test thoroughly - especially async/Sync混合 scenarios
  4. Verify stack traces show cleaner execution paths
  5. Monitor performance in your specific workloads

Best Practices

Do:

  • Use for high-throughput services (web APIs, microservices)
  • Combine with ReadyToRun for AOT compilation benefits
  • Test with both debugger and profiler
  • Monitor async-specific performance counters

Don't:

  • Assume it's production-ready (still preview)
  • Forget to test edge cases with synchronization primitives
  • Expect exception stack traces to change (they remain the same)
  • Use without proper preview feature flags

Conclusion

.NET 11 Runtime Async (V2) solves real pain points developers face daily: elusive async deadlocks, poor debugging experience, and unnecessary performance overhead. By moving async state management to the runtime, Microsoft delivers a cleaner, faster, more reliable async/await experience.

The 50% throughput improvement and 80% faster cold starts make this particularly valuable for cloud-native applications and microservices where every millisecond counts.

💡 About author: https://www.linkedin.com/in/vikrant-bagal

dotnet #csharp #performance #programming #async

Top comments (0)