Description
Background and motivation
The purpose of this proposal is to introduce the changes in the public API as required by the Runtime Async feature.
For the detailed documentation of IL level interface of Runtime Async feature see proposed changes to ECMA-335 document.
For reference, the location of corresponding C# spec - https://github.com/dotnet/roslyn/blob/main/docs/compilers/CSharp/Runtime%20Async%20Design.md
NOTE: referenced documents are work-in-progress, so could be slightly behind on details.
API Proposal
The API adds a new Async
member in the MethodImplOptions
enum, and a corresponding change to System.Reflection.MethodImplAttributes
.
The purpose of this flag is to indicate that a particular method is an Async
method. There are certain restrictions on when this flag is applicable. For example the method must be Task[<T>]
or ValueTask[<T>]
-returning, can't be Synchronized
, etc... The method also needs to satisfy certain correctness invariants in it's body. For example, it must return a value with a type compatible to the unwrapped type of the formal return type in the method signature.
The key effect of a method being Async
is that such method can use a group of Await
functions to delegate the "awaiting" to the runtime.
The Await
methods are special methods that provide functionality not otherwise expressible in IL - namely asynchronous waiting for potentially incomplete tasks or awaiters.
These Await
helper methods are only callable from Async
methods. Calling one of these Await
methods from a method not decorated as MethodImplOptions.Async
is an invalid IL sequence and will result in an appropriate error at IL-compiling or execution time.
NOTE: It is permitted for Await
helpers to call other Await
helpers and some of them do that. That is legal because these helpers are themselves Async
methods. This part makes no difference to the end user, just something worth mentioning.
namespace System.Runtime.CompilerServices
{
[Flags]
public enum MethodImplOptions
{
Unmanaged = 0x0004,
NoInlining = 0x0008,
ForwardRef = 0x0010,
Synchronized = 0x0020,
NoOptimization = 0x0040,
PreserveSig = 0x0080,
AggressiveInlining = 0x0100,
AggressiveOptimization = 0x0200,
InternalCall = 0x1000,
Async = 0x2000,
}
}
namespace System.Reflection
{
public enum MethodImplAttributes
{
IL = 0,
Managed = 0,
Native = 1,
OPTIL = 2,
CodeTypeMask = 3,
Runtime = 3,
ManagedMask = 4,
Unmanaged = 4,
NoInlining = 8,
ForwardRef = 16,
Synchronized = 32,
NoOptimization = 64,
PreserveSig = 128,
AggressiveInlining = 256,
AggressiveOptimization = 512,
InternalCall = 4096,
Async = 8192,
MaxMethodImplVal = 65535,
}
}
namespace System.Runtime.CompilerServices
{
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
[System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5007", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")]
public static partial class AsyncHelpers
{
/// <summary>
/// Suspends evaluation of the enclosing `Async` method until the asynchronous operation represented
/// by the operand completes.
/// </summary>
public static void UnsafeAwaitAwaiter<TAwaiter>(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion { }
/// <summary>
/// Suspends evaluation of the enclosing `Async` method until the asynchronous operation represented
/// by the operand completes.
/// </summary>
public static void AwaitAwaiter<TAwaiter>(TAwaiter awaiter) where TAwaiter : INotifyCompletion { }
/// <summary>
/// Suspends evaluation of the enclosing `Async` method until the asynchronous operation represented
/// by the operand completes.
/// Then execution continues or an exception is thrown according to the kind of completion.
/// </summary>
public static void Await(System.Threading.Tasks.Task task) { }
/// <summary>
/// Suspends evaluation of the enclosing `Async` method until the asynchronous operation represented
/// by the operand completes.
/// Then returns the result value or throws an exception according to the kind of completion.
/// </summary>
public static T Await<T>(System.Threading.Tasks.Task<T> task) { }
/// <summary>
/// Suspends evaluation of the enclosing `Async` method until the asynchronous operation represented
/// by the operand completes.
/// Then execution continues or an exception is thrown according to the kind of completion.
/// </summary>
public static void Await(System.Threading.Tasks.ValueTask task) { }
/// <summary>
/// Suspends evaluation of the enclosing `Async` method until the asynchronous operation represented
/// by the operand completes.
/// Then returns the result value or throws an exception according to the kind of completion.
/// </summary>
public static T Await<T>(System.Threading.Tasks.ValueTask<T> task) { }
/// <summary>
/// Suspends evaluation of the enclosing `Async` method until the asynchronous operation represented
/// by the operand completes.
/// Then execution continues or an exception is thrown according to the kind of completion.
/// The execution and synchronization context flow will be arranged according to the configuration of
/// the awaitable
/// </summary>
public static void Await(System.Runtime.CompilerServices.ConfiguredTaskAwaitable configuredAwaitable) { }
/// <summary>
/// Suspends evaluation of the enclosing `Async` method until the asynchronous operation represented
/// by the operand completes.
/// Then returns the result value or throws an exception according to the kind of completion.
/// The execution and synchronization context flow will be arranged according to the configuration of
/// the awaitable
/// </summary>
public static T Await<T>(System.Runtime.CompilerServices.ConfiguredTaskAwaitable<T> configuredAwaitable) { }
/// <summary>
/// Suspends evaluation of the enclosing `Async` method until the asynchronous operation represented
/// by the operand completes.
/// Then execution continues or an exception is thrown according to the kind of completion.
/// The execution and synchronization context flow will be arranged according to the configuration of
/// the awaitable
/// </summary>
public static void Await(System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable configuredAwaitable) { }
/// <summary>
/// Suspends evaluation of the enclosing `Async` method until the asynchronous operation represented
/// by the operand completes.
/// Then returns the result value or throws an exception according to the kind of completion.
/// The execution and synchronization context flow will be arranged according to the configuration of
/// the awaitable
/// </summary>
public static T Await<T>(System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable<T> configuredAwaitable) { }
}
}
- We intend to mark this API as
Experimental
until fully supported and available on all runtime flavors. SYSLIB5007
is just the next experimental warning ID available right now.
The warning will say something like:Runtime Async is experimental
API Usage
The API is not designed or optimized for using directly in the code. The intended purpose is to be used by IL generators such as C# compiler and similar.
From the runtime perspective, there is nothing wrong with invoking the API directly, although compilers may put restrictions on direct use of this API.
Here is an example of a typical use.
When Runtime Async code generation is enabled, C# compiler will emit the following code:
public async Task<int> M1(int arg)
{
return await M2(arg);
}
public async Task<int> M2(int arg)
{
await Task.Yield();
ValueTask<int> vt = ValueTask.FromResult(arg);
return await vt.ConfigureAwait(false);
}
as an IL equivalent of this:
[MethodImpl(MethodImplOptions.Async)]
public Task<int> M1(int arg)
{
return AsyncHelpers.Await<int>(M2(arg));
}
[MethodImpl(MethodImplOptions.Async)]
public Task<int> M2(int arg)
{
YieldAwaitable.YieldAwaiter awaiter = Task.Yield().GetAwaiter();
if (!awaiter.IsCompleted)
{
AsyncHelpers.UnsafeAwaitAwaiterFromRuntimeAsync<YieldAwaitable.YieldAwaiter>(awaiter);
}
awaiter.GetResult();
ValueTask<int> vt = ValueTask.FromResult(arg);
return AsyncHelpers.Await<int>(vt.ConfigureAwait(false));
}
Alternative Designs
This API has evolved from an alternative design where the "await" in Async methods would be encoded by calling Task-returning methods via an alternative signature.
In addition to that, the Async
methods were defined by using an alternative signature as well.
Compared to the scheme that encodes await
through Await
helpers, that scheme had a few inconveniences:
- it is more confusing for IL generator to call methods that "do not exist"
- similar additional complications exist in overriding/hiding/implementing scenarios as IL generator needs to understand both signature flavors when reasoning about bases and interfaces.
- using alternative, and previously illegal, signatures has more potential for breaking existing tooling. Even if tools are unconcerned about method bodies.
- IL generator needs to be aware if the awaited argument is a method call (not a field, local, ...), as only a method could have an alternative signature.
- the scenario with configured awaiters did not have a good solution.
Overall, this API requires slightly more complexity on VM and JIT implementation side, while being much friendlier to IL generators.
Risks
Current implementation is CoreCLR-only and employs JIT compiler to perform transformation of async methods into state machines and to provide thunks for interop with ordinary Task-based asynchronous operations.
There is some risk that implementing semantics of this API could be harder on non-JIT runtimes. However, we do not see fundamental reasons why the same semantic could not be implemented in AOT or in Interpreter mode.