Preview 2 brings significant progress toward runtime-native async. Instead of the compiler generating state-machine classes, the runtime itself manages async suspension and resumption, producing cleaner stack traces, better debuggability, and lower overhead.
Runtime async is still a preview feature. The compiler must emit methods with MethodImplOptions.Async for the runtime to treat them as runtime-async. Today this requires a compiler feature flag and the preview-features opt-in:
<PropertyGroup>
<Features>runtime-async=on</Features>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
</PropertyGroup>The most visible difference is in live stack traces — what profilers, debuggers, and new StackTrace() see during execution. With compiler-generated async, every async method produces multiple frames for state-machine infrastructure. With runtime async, the real methods appear directly on the call stack.
// async-test.csproj:
// <Features>runtime-async=on</Features>
// <EnablePreviewFeatures>true</EnablePreviewFeatures>
using System.Diagnostics;
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;
Console.WriteLine(new StackTrace(fNeedFileInfo: true));
}Without runtime-async — 13 frames, state-machine infrastructure visible:
at Program.<<Main>$>g__InnerAsync|0_2() in Program.cs:line 24
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.<<Main>$>g__InnerAsync|0_2()
at Program.<<Main>$>g__MiddleAsync|0_1() in Program.cs:line 14
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.<<Main>$>g__MiddleAsync|0_1()
at Program.<<Main>$>g__OuterAsync|0_0() in Program.cs:line 8
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.<<Main>$>g__OuterAsync|0_0()
at Program.<Main>$(String[] args) in Program.cs:line 3
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.<Main>$(String[] args)
at Program.<Main>(String[] args)
With runtime-async — 5 frames, the real call chain:
at Program.<<Main>$>g__InnerAsync|0_2() in Program.cs:line 24
at Program.<<Main>$>g__MiddleAsync|0_1() in Program.cs:line 14
at Program.<<Main>$>g__OuterAsync|0_0() in Program.cs:line 8
at Program.<Main>$(String[] args) in Program.cs:line 3
at Program.<Main>(String[] args)
(Note: methods are named $>g__ because they are local functions, not because they are async)
This improvement benefits profiling tools, diagnostic logging, and the debugger call stack window — anything that inspects the live execution stack rather than exception traces.
Note: Exception stack traces (
catch (Exception ex) { Console.WriteLine(ex); }) already look the same with or without runtime async, thanks to existingExceptionDispatchInfocleanup in the compiler-generated code. The improvement is in what you see during execution.
Breakpoints now bind correctly inside runtime-async methods, and the debugger can step through await boundaries without jumping into compiler-generated infrastructure. This is the result of dotnet/runtime #123644, which teaches the debugger to recognize async thunks and map them back to the original source locations.
The JIT now eliminates bounds checks for the common pattern where an index plus a constant is compared against a length. (dotnet/runtime #124242)
Checked arithmetic contexts that the JIT can prove are redundant (e.g. the value is already in range) are now optimized away. (dotnet/runtime #124147, dotnet/runtime #124184)
ReadyToRun images can now devirtualize non-shared generic virtual method calls, improving ahead-of-time compiled code performance. (dotnet/runtime #123183)
New ARM SVE2 intrinsics: ShiftRightLogicalNarrowingSaturate(Even|Odd). These require both JIT support and managed API surface. (dotnet/runtime #123888)
Interface dispatch on platforms that lack JIT support (e.g. iOS) was falling back to an expensive generic fixup path. Enabling cached dispatch by default yields up to 200x improvements in interface-heavy code on these targets. (dotnet/runtime #123776)
Switches from reading /dev/urandom to the getrandom() syscall with batch caching, yielding roughly a 12% throughput improvement for GUID generation on Linux. (dotnet/runtime #123540)
A new --dynamiccodecompiled false build option configures CoreCLR + crossgen2 to behave like iOS (JIT disabled, interpreter enabled, cached interface dispatch), enabling desktop testing of iOS-like scenarios without device deployment. (dotnet/runtime #124168)