docs/compilers/CSharp/Runtime Async Design.md
See also the ECMA-335 specification change for this feature: https://github.com/dotnet/runtime/blob/main/docs/design/specs/runtime-async.md. https://github.com/dotnet/runtime/issues/109632 tracks open issues for the feature in the runtime.
This document goes over the general design of how Roslyn works with the Runtime Async feature to produce IL. In general, we try to avoid exposing this feature at the user level; initial binding is almost entirely unaffected by runtime async. Exposed symbols do not give direct information about whether they were compiled with runtime async, and indeed the compiler has no idea whether a method from a referenced assembly is compiled with runtime async or not.
We use the following helper APIs to indicate suspension points to the runtime, in addition to the runtime async call syntax:
namespace System.Runtime.CompilerServices;
namespace System.Runtime.CompilerServices;
[System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5007", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")]
public static partial class AsyncHelpers
{
public static void UnsafeAwaitAwaiter<TAwaiter>(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion { }
public static void AwaitAwaiter<TAwaiter>(TAwaiter awaiter) where TAwaiter : INotifyCompletion { }
// These methods are used to directly await method calls
public static void Await(System.Threading.Tasks.Task task) { }
public static T Await<T>(System.Threading.Tasks.Task<T> task) { }
public static void Await(System.Threading.Tasks.ValueTask task) { }
public static T Await<T>(System.Threading.Tasks.ValueTask<T> task) { }
public static void Await(System.Runtime.CompilerServices.ConfiguredTaskAwaitable configuredAwaitable) { }
public static T Await<T>(System.Runtime.CompilerServices.ConfiguredTaskAwaitable<T> configuredAwaitable) { }
public static void Await(System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable configuredAwaitable) { }
public static T Await<T>(System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable<T> configuredAwaitable) { }
}
The presence of these also drive whether the feature can be used. These APIs must be defined in the same assembly that defines object, and the assembly cannot reference any other assemblies. In terms of
CoreFX, this means it must be defined in the System.Runtime reference assembly.
We presume the following MethodImplOptions bit is present when AsyncHelpers is defined. This is used to indicate to the JIT that it should generate an async state machine for the method. This bit is not allowed
to be used manually on any method; it is added by the compiler to an async method.
TODO: We may want to block directly calling MethodImplOptions.Async methods with non-Task/ValueTask return types.
namespace System.Runtime.CompilerServices;
public enum MethodImplOptions
{
Async = 0x2000
}
For experimentation purposes, we recognize an attribute that can be used to force the compiler to generate the runtime async code, or to force the compiler to generate a full state machine. This attribute is not defined in the BCL, and exists as an escape hatch for experimentation. It may be removed when the feature ships in stable.
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Method)]
public class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync) : Attribute();
As mentioned previously, we try to expose as little of this to initial binding as possible. The one major exception to this is our handling of the MethodImplOption.Async; we do not let this be applied to
user code, and will issue an error if a user tries to do this by hand.
Compiler generated async state machines and runtime generated async share some of the same building blocks. Both need to have awaits with in catch and finally blocks rewritten to pend the exceptions,
perform the await outside of the catch/finally region, and then have the exceptions restored as necessary.
TODO: Go over IAsyncEnumerable and confirm that the initial rewrite to a Task-based method produces code that can then be implemented with runtime async, rather than a full compiler state machine.
TODO: Clarify with the debugger team where NOPs need to be inserted for debugging/ENC scenarios.
We will likely need to insert AwaitYieldPoint and AwaitResumePoints for the scenarios where we emit calls to AsyncHelpers async helpers, but can we avoid them for calls in runtime async form?
Below are some examples of what IL is generated for specific examples.
TODO: Include debug versions
In general, an async method declared in C# will be transformed as follows:
async Task M()
{
// ...
}
[MethodImpl(MethodImplOptions.Async)]
Task M()
{
// ... see lowering strategy for each kind of await below ...
}
The same holds for methods that return Task<T>, ValueTask, and ValueTask<T>. Any method returning a different Task-like type is not transformed to runtime async form and uses a C#-generated state machine.
awaits within the body will either be transformed to Runtime-Async call format (as detailed in the runtime specification), or we will use one of the AsyncHelpers methods to do the await. Specifics
for given scenarios are elaborated in more detail below.
TODO: Async iterators (returning IAsyncEnumerable<T>)
AsyncHelpers.Await ScenariosFor any await expr with where expr has type E, the compiler will attempt to match it to a helper method in System.Runtime.CompilerServices.AsyncHelpers. The following algorithm is used:
E has generic arity greater than 1, no match is found and instead move to await any other type.System.Runtime.CompilerServices.AsyncHelpers from corelib (the library that defines System.Object and has no references) is fetched.Await are put into a group called M.Mi in M:
Mi's generic arity does not match E, it is removed.Mi takes more than 1 parameter (named P), it is removed.Mi has a generic arity of 0, all of the following must be true, or Mi is removed:
System.VoidE to the type of P.Mi has a generic arity of 1 with type param Tm, all of the following must be true, or Mi is removed:
TmE is TeTi satisfies any constraints on TmMie is Mi with Te substituted for Tm, and Pe is the resulting parameter of MieE to the type of PeMi remains, that method is used for the following rewrites. Otherwise, we instead move to await any other type.We'll generally rewrite await expr into System.Runtime.CompilerServices.AsyncHelpers.Await(expr). A number of different example scenarios for this are covered below. The
main interesting deviations are when struct rvalues need to be hoisted across an await, and exception handling rewriting.
These rules are intended to cover the following types:
Task, or any subtypes of TaskTask<T>, or any subtypes of Task<T>ValueTaskValueTask<T>ConfiguredTaskAwaitableConfiguredTaskAwaitable<T>ConfiguredValueTaskAwaitableConfiguredValueTaskAwaitable<T>Task-like types the runtime would like to intrinsifyTask-returning methodclass C
{
static Task M();
}
await C.M();
Translated C#:
System.Runtime.CompilerServices.AsyncHelpers.Await(C.M());
call [System.Runtime]System.Threading.Tasks.Task C::M()
call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
var c = new C();
await c.M();
class C
{
Task M();
}
Translated C#:
var c = new C();
System.Runtime.CompilerServices.AsyncHelpers.Await(c.M());
newobj instance void C::.ctor()
callvirt instance class [System.Runtime]System.Threading.Tasks.Task C::M()
call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
T Task<T>-returning methodint i = await C.M();
class C
{
static Task<int> M();
}
Translated C#:
int i = System.Runtime.CompilerServices.AsyncHelpers.Await<int>(C.M());
call class [System.Runtime]System.Threading.Tasks.Task`1<int32> C::M()
call int32 [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await<int32>(class [System.Runtime]System.Threading.Tasks.Task`1<int32>)
stloc.0
var c = new C();
int i = await c.M();
class C
{
Task<int> M();
}
Translated C#:
var c = new C();
int i = System.Runtime.CompilerServices.AsyncHelpers.Await<int>(c.M());
newobj instance void C::.ctor()
callvirt instance class [System.Runtime]System.Threading.Tasks.Task`1<int32> C::M()
call int32 [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await<int32>(class [System.Runtime]System.Threading.Tasks.Task`1<int32>)
stloc.0
Taskvar local = M();
await local;
class C
{
static Task M();
}
Translated C#:
var local = C.M();
System.Runtime.CompilerServices.AsyncHelpers.Await(local);
{
.locals init (
[0] valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter awaiter
)
IL_0000: call class [System.Runtime]System.Threading.Tasks.Task C::M()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
IL_000c: ret
}
Task<T>var local = M();
var i = await local;
class C
{
static Task<int> M();
}
Translated C#:
var local = C.M();
var i = System.Runtime.CompilerServices.AsyncHelpers.Await<int>(local);
{
.locals init (
[0] class [System.Runtime]System.Threading.Tasks.Task`1<int32> local,
[1] int32 i
)
IL_0000: call class [System.Runtime]System.Threading.Tasks.Task`1<int32> C::M()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: call !!0 [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await<int32>(class [System.Runtime]System.Threading.Tasks.Task`1<!!0>)
IL_000c: stloc.1
IL_000d: ret
}
T-returning methodawait C.M<Task>();
class C
{
static T M<T>();
}
Translated C#:
System.Runtime.CompilerServices.AsyncHelpers.Await(C.M<Task>());
{
IL_0000: call !!0 C::M<class [System.Runtime]System.Threading.Tasks.Task>()
IL_0005: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
IL_000a: ret
}
T Task<T>-returning methodint i = await C.M<int>();
class C
{
static Task<T> M<T>();
}
Translated C#:
int i = System.Runtime.CompilerServices.AsyncHelpers.Await<int>(C.M<int>());
{
IL_0000: call class [System.Runtime]System.Threading.Tasks.Task`1<!!0> C::M<int32>()
IL_0005: call !!0 [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await<int32>(class [System.Runtime]System.Threading.Tasks.Task`1<!!0>)
IL_000a: stloc.0
IL_000b: ret
}
Task-returning delegateAsyncDelegate d = C.M;
await d();
delegate Task AsyncDelegate();
class C
{
static Task M();
}
Translated C#
AsyncDelegate d = C.M;
System.Runtime.CompilerServices.AsyncHelpers.Await(d());
{
IL_0000: ldsfld class AsyncDelegate Program/'<>O'::'<0>__M'
IL_0005: dup
IL_0006: brtrue.s IL_001b
IL_0008: pop
IL_0009: ldnull
IL_000a: ldftn class [System.Runtime]System.Threading.Tasks.Task C::M()
IL_0010: newobj instance void AsyncDelegate::.ctor(object, native int)
IL_0015: dup
IL_0016: stsfld class AsyncDelegate Program/'<>O'::'<0>__M'
IL_001b: callvirt instance class [System.Runtime]System.Threading.Tasks.Task AsyncDelegate::Invoke()
IL_0020: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
IL_0025: ret
}
T-returning delegate where T becomes TaskFunc<Task> d = C.M;
await d();
class C
{
static Task M();
}
Translated C#:
Func<Task> d = C.M;
System.Runtime.CompilerServices.AsyncHelpers.Await(d());
{
IL_0000: ldsfld class [System.Runtime]System.Func`1<class [System.Runtime]System.Threading.Tasks.Task> Program/'<>O'::'<0>__M'
IL_0005: dup
IL_0006: brtrue.s IL_001b
IL_0008: pop
IL_0009: ldnull
IL_000a: ldftn class [System.Runtime]System.Threading.Tasks.Task C::M()
IL_0010: newobj instance void class [System.Runtime]System.Func`1<class [System.Runtime]System.Threading.Tasks.Task>::.ctor(object, native int)
IL_0015: dup
IL_0016: stsfld class [System.Runtime]System.Func`1<class [System.Runtime]System.Threading.Tasks.Task> Program/'<>O'::'<0>__M'
IL_001b: callvirt instance !0 class [System.Runtime]System.Func`1<class [System.Runtime]System.Threading.Tasks.Task>::Invoke()
IL_0020: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
IL_0025: ret
}
catch blocktry
{
throw new Exception();
}
catch (Exception ex)
{
await C.M();
throw;
}
class C
{
static Task M();
}
Translated C#:
int pendingCatch = 0;
Exception pendingException;
try
{
throw new Exception();
}
catch (Exception e)
{
pendingCatch = 1;
pendingException = e;
}
if (pendingCatch == 1)
{
System.Runtime.CompilerServices.AsyncHelpers.Await(C.M());
throw pendingException;
}
{
.locals init (
[0] int32 pendingCatch,
[1] class [System.Runtime]System.Exception pendingException
)
IL_0000: ldc.i4.0
IL_0001: stloc.0
.try
{
IL_0002: newobj instance void [System.Runtime]System.Exception::.ctor()
IL_0007: throw
} // end .try
catch [System.Runtime]System.Exception
{
IL_0008: ldc.i4.1
IL_0009: stloc.0
IL_000a: stloc.1
IL_000b: leave.s IL_000d
} // end handler
IL_000d: ldloc.0
IL_000e: ldc.i4.1
IL_000f: bne.un.s IL_001d
IL_0011: call class [System.Runtime]System.Threading.Tasks.Task C::M()
IL_0016: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
IL_001b: ldloc.1
IL_001c: throw
IL_001d: ret
}
finally blocktry
{
throw new Exception();
}
finally
{
await C.M();
}
class C
{
static Task M();
}
Translated C#:
Exception pendingException;
try
{
throw new Exception();
}
catch (Exception e)
{
pendingException = e;
}
System.Runtime.CompilerServices.AsyncHelpers.Await(C.M());
if (pendingException != null)
{
throw pendingException;
}
{
.locals init (
[0] class [System.Runtime]System.Exception pendingException
)
.try
{
IL_0000: newobj instance void [System.Runtime]System.Exception::.ctor()
IL_0005: throw
} // end .try
catch [System.Runtime]System.Exception
{
IL_0006: stloc.0
IL_0007: leave.s IL_0009
} // end handler
IL_0009: call class [System.Runtime]System.Threading.Tasks.Task C::M()
IL_000e: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
IL_0013: ldloc.0
IL_0014: brfalse.s IL_0018
IL_0016: ldloc.0
IL_0017: throw
IL_0018: ret
}
int[] a = new int[] { };
a[C.M2()] += await C.M1();
class C
{
public static Task<int> M1();
public static int M2();
}
Translated C#:
int[] a = new int[] { };
int _tmp1 = C.M2();
int _tmp2 = a[_tmp1];
int _tmp3 = System.Runtime.CompilerServices.AsyncHelpers.Await(C.M1());
a[_tmp1] = _tmp2 + _tmp3;
{
.locals init (
[0] int32 _tmp1,
[1] int32 _tmp2,
[2] int32 _tmp3
)
IL_0000: ldc.i4.0
IL_0001: newarr [System.Runtime]System.Int32
IL_0006: call int32 C::M2()
IL_000b: stloc.0
IL_000c: dup
IL_000d: ldloc.0
IL_000e: ldelem.i4
IL_000f: stloc.1
IL_0010: call class [System.Runtime]System.Threading.Tasks.Task`1<int32> C::M1()
IL_0015: call !!0 [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::Await<int32>(class [System.Runtime]System.Threading.Tasks.Task`1<!!0>)
IL_001a: stloc.2
IL_001b: ldloc.0
IL_001c: ldloc.1
IL_001d: ldloc.2
IL_001e: add
IL_001f: stelem.i4
IL_0020: ret
}
For anything that isn't a Task, Task<T>, ValueTask, and ValueTask<T>, we instead use System.Runtime.CompilerServices.AsyncHelpers.AwaitAwaiterFromRuntimeAsync or
System.Runtime.CompilerServices.AsyncHelpers.UnsafeAwaitAwaiterFromRuntimeAsync. These are covered below.
ICriticalNotifyCompletion lowering is always preferred over INotifyCompletion lowering, when we statically know ICriticalNotifyCompletion is implemented by the expression.
var c = new C();
await c;
class C
{
public class Awaiter : ICriticalNotifyCompletion
{
public void OnCompleted(Action continuation) { }
public void UnsafeOnCompleted(Action continuation) { }
public bool IsCompleted => true;
public void GetResult() { }
}
public Awaiter GetAwaiter() => new Awaiter();
}
Translated C#:
var c = new C();
_ = {
var awaiter = c.GetAwaiter();
if (!awaiter.IsCompleted)
{
System.Runtime.CompilerServices.AsyncHelpers.UnsafeAwaitAwaiterFromRuntimeAsync<C.Awaiter>(awaiter);
}
awaiter.GetResult()
};
{
.locals init (
[0] class C/Awaiter awaiter
)
IL_0000: newobj instance void C::.ctor()
IL_0005: callvirt instance class C/Awaiter C::GetAwaiter()
IL_000a: stloc.0
IL_000b: ldloc.0
IL_000c: callvirt instance bool C/Awaiter::get_IsCompleted()
IL_0011: brtrue.s IL_0019
IL_0013: ldloc.0
IL_0014: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::UnsafeAwaitAwaiterFromRuntimeAsync<class C/Awaiter>(!!0)
IL_0019: ldloc.0
IL_001a: callvirt instance void C/Awaiter::GetResult()
IL_001f: ret
}
var c = new C();
await c;
class C
{
public class Awaiter : INotifyCompletion
{
public void OnCompleted(Action continuation) { }
public bool IsCompleted => true;
public void GetResult() { }
}
public Awaiter GetAwaiter() => new Awaiter();
}
Translated C#:
var c = new C();
_ = {
var awaiter = c.GetAwaiter();
if (!awaiter.IsCompleted)
{
System.Runtime.CompilerServices.AsyncHelpers.AwaitAwaiterFromRuntimeAsync<C.Awaiter>(awaiter);
}
awaiter.GetResult()
};
{
.locals init (
[0] class C/Awaiter awaiter
)
IL_0000: newobj instance void C::.ctor()
IL_0005: callvirt instance class C/Awaiter C::GetAwaiter()
IL_000a: stloc.0
IL_000b: ldloc.0
IL_000c: callvirt instance bool C/Awaiter::get_IsCompleted()
IL_0011: brtrue.s IL_0019
IL_0013: ldloc.0
IL_0014: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::AwaitAwaiterFromRuntimeAsync<class C/Awaiter>(!!0)
IL_0019: ldloc.0
IL_001a: callvirt instance void C/Awaiter::GetResult()
IL_001f: ret
}