Skip to content

[API Proposal]: Public API for the Runtime Async #114310

Closed
@VSadov

Description

@VSadov

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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions