Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update runtime async spec #76872

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 99 additions & 55 deletions docs/compilers/CSharp/Runtime Async Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,32 @@ We use the following helper APIs to indicate suspension points to the runtime, i
```cs
namespace System.Runtime.CompilerServices;

// These methods are used to await things that cannot use runtime async signature form
// TODO: Clarify which of these should be preferred? Should we always emit the `Unsafe` version when awaiting something that implements `ICriticalNotifyCompletion`?
namespace System.Runtime.CompilerServices;

public static class RuntimeHelpers
{
[RuntimeAsyncMethod]
// These methods are used to await things that cannot use the Await helpers below
[MethodImpl(MethodImplOptions.Async)]
public static Task AwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : INotifyCompletion;
[RuntimeAsyncMethod]
[MethodImpl(MethodImplOptions.Async)]
public static Task UnsafeAwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion;
333fred marked this conversation as resolved.
Show resolved Hide resolved

// These methods are used to directly await method calls
// TODO: Do we only use these when directly awaiting a method call, or do we always use these when a `Task` is awaited, even if that task is a local?
333fred marked this conversation as resolved.
Show resolved Hide resolved
[MethodImpl(MethodImplOptions.Async)]
public static void Await(Task task);
[MethodImpl(MethodImplOptions.Async)]
public static T Await<T>(Task<T> task);
[MethodImpl(MethodImplOptions.Async)]
public static void Await(ValueTask task);
[MethodImpl(MethodImplOptions.Async)]
public static T Await<T>(ValueTask<T> task);
333fred marked this conversation as resolved.
Show resolved Hide resolved
}
```

We presume the following `MethodImplOptions` bit is present. This is used to indicate to the JIT that it should generate an async state machine for the method.
We presume the following `MethodImplOptions` bit is present. 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.
333fred marked this conversation as resolved.
Show resolved Hide resolved

```cs
namespace System.Runtime.CompilerServices;
Expand Down Expand Up @@ -77,6 +89,8 @@ public class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync) : Attribut
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.

TODO: We may need special handling for the implementation of the `RuntimeHelpers.Await` methods in corelib to permit usage of `MethodImplOptions.Async` directly, as they will not be `async` as we think of it in C#.

Compiler generated async state machines and runtime generated async share some of the same building blocks. Both need to have `await`s 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.

Expand Down Expand Up @@ -132,8 +146,15 @@ class C
await C.M();
```

Translated C#:

```cs
System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M());
```

```il
call modreq(class [System.Runtime]System.Threading.Tasks.Task) void C::M()
call [System.Runtime]System.Threading.Tasks.Task C::M()
call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
```

---------------------------
Expand All @@ -148,9 +169,17 @@ class C
}
```

Translated C#:

```cs
var c = new C();
System.Runtime.CompilerServices.RuntimeHelpers.Await(c.M());
```

```il
newobj instance void C::.ctor()
callvirt instance modreq(class [System.Runtime]System.Threading.Tasks.Task) void C::M()
callvirt instance class [System.Runtime]System.Threading.Tasks.Task C::M()
call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
```

#### Await a concrete `T` `Task<T>`-returning method
Expand All @@ -164,8 +193,15 @@ class C
}
```

Translated C#:

```cs
int i = System.Runtime.CompilerServices.RuntimeHelpers.Await<int>(C.M());
```

```il
call modreq(class [System.Runtime]System.Threading.Tasks.Task`1<int32>) int32 C::M()
call class [System.Runtime]System.Threading.Tasks.Task`1<int32> C::M()
call int32 [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await<int32>(class [System.Runtime]System.Threading.Tasks.Task`1<int32>)
stloc.0
```

Expand All @@ -181,9 +217,17 @@ class C
}
```

Translated C#:

```cs
var c = new C();
int i = System.Runtime.CompilerServices.RuntimeHelpers.Await<int>(c.M());
```

```il
newobj instance void C::.ctor()
callvirt instance modreq(class [System.Runtime]System.Threading.Tasks.Task`1<int32>) int32 C::M()
callvirt instance class [System.Runtime]System.Threading.Tasks.Task`1<int32> C::M()
call int32 [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await<int32>(class [System.Runtime]System.Threading.Tasks.Task`1<int32>)
stloc.0
```

Expand Down Expand Up @@ -390,7 +434,7 @@ catch (Exception e)

if (pendingCatch == 1)
{
/* Runtime-Async Call */ C.M();
System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M());
throw pendingException;
}
```
Expand All @@ -402,29 +446,31 @@ if (pendingCatch == 1)
[1] class [System.Runtime]System.Exception pendingException
)

IL_0000: ldc.i4.0
IL_0001: stloc.0
.try
{
IL_0000: newobj instance void [System.Runtime]System.Exception::.ctor()
IL_0005: throw
}
IL_0002: newobj instance void [System.Runtime]System.Exception::.ctor()
IL_0007: throw
} // end .try
catch [System.Runtime]System.Exception
{
IL_0006: stloc.1
IL_0007: ldc.i4.1
IL_0008: stloc.0
IL_0009: leave.s IL_000b
}
IL_0008: ldc.i4.1
IL_0009: stloc.0
IL_000a: stloc.1
IL_000b: leave.s IL_000d
} // end handler

IL_000b: ldloc.0
IL_000c: ldc.i4.1
IL_000d: bne.un.s IL_0017
IL_000d: ldloc.0
IL_000e: ldc.i4.1
IL_000f: bne.un.s IL_001d

IL_000f: ldloc.1
IL_0010: call modreq(class [System.Runtime]System.Threading.Tasks.Task) void C::M()
IL_0015: pop
IL_0016: throw
IL_0011: call class [System.Runtime]System.Threading.Tasks.Task C::M()
IL_0016: call void System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
IL_001b: ldloc.1
IL_001c: throw

IL_0017: ret
IL_001d: ret
}
```

Expand Down Expand Up @@ -459,7 +505,7 @@ catch (Exception e)
pendingException = e;
}

/* Runtime-Async Call */ C.M();
System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M());

if (pendingException != null)
{
Expand All @@ -477,22 +523,22 @@ if (pendingException != null)
{
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 modreq(class [System.Runtime]System.Threading.Tasks.Task) void C::M()
IL_000e: pop
IL_000f: ldloc.0
IL_0010: brfalse.s IL_0014
IL_0009: call class [System.Runtime]System.Threading.Tasks.Task C::M()
IL_000e: call void System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task)
IL_0013: ldloc.0
IL_0014: brfalse.s IL_0018

IL_0012: ldloc.0
IL_0013: throw
IL_0016: ldloc.0
IL_0017: throw

IL_0014: ret
IL_0018: ret
}
```

Expand All @@ -515,36 +561,34 @@ Translated C#:
int[] a = new int[] { };
int _tmp1 = C.M2();
int _tmp2 = a[_tmp1];
int _tmp3 = /* Runtime-Async Call */ C.M1();
int _tmp3 = System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M1());
a[_tmp1] = _tmp2 + _tmp3;
```

```il
{
.locals init (
[0] int32[] a,
[1] int32 _tmp1,
[2] int32 _tmp2,
[3] int32 _tmp3
[0] int32 _tmp1,
[1] int32 _tmp2,
[2] int32 _tmp3
)

IL_0000: ldc.i4.0
IL_0001: newarr [System.Runtime]System.Int32
IL_0006: stloc.0
IL_0007: call int32 C::M2()
IL_000c: stloc.1
IL_0006: call int32 C::M2()
IL_000b: stloc.0
IL_000c: dup
IL_000d: ldloc.0
IL_000e: ldloc.1
IL_000f: ldelem.i4
IL_0010: stloc.2
IL_0011: call modreq(class [System.Runtime]System.Threading.Tasks.Task<int32>) int32 C::M1()
IL_0016: stloc.3
IL_0017: ldloc.0
IL_0018: ldloc.1
IL_0019: ldloc.2
IL_001a: ldloc.3
IL_001b: add
IL_001c: stelem.i4
IL_001d: ret
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.CompilerServices.RuntimeHelpers::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
}
```