-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Propose new async API #110420
base: main
Are you sure you want to change the base?
Propose new async API #110420
Conversation
This PR replaces the 'method signature' overloading with a new 'Await' method that JIT should treat as an intrinsic. The primary benefit in this case is that it makes the IL valid according to previous versions of the ECMA specification. Ideally, this should cause fewer failures in tooling that examines IL.
Tagging subscribers to this area: @dotnet/area-meta |
docs/design/specs/runtime-async.md
Outdated
```C# | ||
namespace System.Runtime.CompilerServices | ||
{ | ||
public static class RuntimeHelpers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What assembly will define these? Should we restrict it to just system.private.corelib?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In practice, this class already exists and is in corelib, so we will put these in corelib. As far as ECMA is concerned, I see no reason to mandate it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there's no reason to mandate it, that must mean that the runtime needs to accept a user defining these helpers themselves, rather than the ones the runtime defines. That seems... Unlikely to be what you want.
docs/design/specs/runtime-async.md
Outdated
|
||
Callers may retrieve a Task/ValueTask return type from an async method via calling its primary, definitional signature. This functionality is available in both sync and async methods. | ||
These methods are also only legal to call inside async methods. These methods perform suspension like the `AwaitAwaiter...` methods, but are optimized for calling on the return value of a call to an async method. To achieve maximum performance, the IL sequence of two `call` instructions -- one to the async method and immediately one to the `Await` method -- should be preferred. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this interact with ConfigureAwait? It seems like 99.999% of all use of await in our core libraries would not use these specialty methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the things that the group proposed is that the runtime may be able to understand the pattern of RuntimeHelpers.Await(taskMethod().ConfigureAwait(false))
and avoid the Task
materialization in that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but that doesn't quite fit with the proposed APIs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main problem is that what comes out of ConfigureAwait
isn't a task, it's a ConfiguredTaskAwaitable
. The only way we have of dealing with this in the current design is to use the AwaitAwaiter...
API.
I agree that ConfigureAwait
is something we should handle, it's just that it's not a 'new' problem with our API structure.
One option is to add an Await
for ConfigureTaskAwaitable
as above, and then recognize the triple sequence above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to raise a question about what the value of this Await
function is, over recognizing RuntimeHelpers.AwaitAwaiterFromRuntimeAsync(Async2Function().GetAwaiter())
and RuntimeHelpers.AwaitAwaiterFromRuntimeAsync(Async2Function().ConfigureAwait(constant).GetAwaiter())
.
Size comes to mind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose AwaitAwaiterFromRuntimeAsync
does not really have the right shape to be used in the way we'd like here, and it's not easy to generalize it to have a shape that would be usable either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good point. This was implicit, but let's make it explicit. I see the value of Await
as:
- Size of IL
- Complexity (a clearer sequence to recognize. If we open up GetAwaiter we need to also think about entire blocks like
if (!awaiter.IsCompleted) { awaiter.UnsafeAwaitAwaiterFromRuntimeAsync(); }
.Await
is comparatively simpler) - Shorter/simpler sequence for the JIT
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That second point is particularly relevant. I expect that's the general pattern that Roslyn will emit for such locations.
Maybe also recognize the |
Two things here:
I think we should understand the case where we are actually helping an unaware IL rewriter keep working, vs creating a situation that makes things worse and the only fix, regardless of what we do, is for the IL rewriter to fix their code. |
[MethodImpl(MethodImplOptions.Async)] | ||
public static T Await<T>(Task<T> task); | ||
[MethodImpl(MethodImplOptions.Async)] | ||
public static T Await<T>(ValueTask<T> task); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the MethodImpl
part is uninteresting for the spec – it really is an implementation detail of SPC whether or not this bit is set for these, and consumers should not be using it to make any decisions. Similarly for AwaitAwaiterFromRuntimeAsync
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. The only thing that might be useful is some sort of signifier meaning "async method only"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Await
cannot be formally an async method, since it would need to return Task.
} | ||
} | ||
``` | ||
|
||
Each of the above methods will have semantics analogous to the current AsyncTaskMethodBuilder.AwaitOnCompleted/AwaitUnsafeOnCompleted methods. After calling this method, it can be presumed that the task has completed. These methods are only legal to call from inside async methods. | ||
These methods are only legal to call inside async methods. The `...AwaitAwaiter...` methods will have semantics analogous to the current `AsyncTaskMethodBuilder.AwaitOnCompleted/AwaitUnsafeOnCompleted` methods. After calling either method, it can be presumed that the task or awaiter has completed. The `Await` methods perform suspension like the `AwaitAwaiter...` methods, but are optimized for calling on the return value of a call to an async method. To achieve maximum performance, the IL sequence of two `call` instructions -- one to the async method and immediately one to the `Await` method -- should be preferred. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These methods are only legal to call inside async methods
From a language perspective, we'd probably have to make an actual change to C# enforce this as a compiler error. We could do a warning without any issue though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth putting something like a modreq(AsyncOnly)
on it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could, though I'm not sure that would actually help. It's still technically a language change to forbid calling the method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's a language change -- the compiler is free to provide more errors than mandated by the spec. For example, "this program is too complex to compile" or the errors provided around usage of the ParamArrayAttribute and similar.
@jakobbotsch Could you take a look at this and give your thoughts? Hoping to move forward or close this out soon. |
I think this is not exactly what is achieved here. Current IL at the call sites is actually "valid" - as in "we call a method that returns T and get result typed as T". The part that the called method (that returns Besides, we will still need to special case async method bodies to require that returned values be compatible with unwarapped return type in the signature. That seems acceptable, but also means that IL tools will still need to learn a few things about async methods. I think the main benefit of the proposed call site encoding is that the entire FWIW it would be possible to choose different ways to represent helper/thunks internally if needed. |
Now, if we agree on benefits, what are the disadvantages? My main concerns are:
What about
Or better - it could have the same implementation as what we emit for thuncs (re: Something like: // Marked intrinsic since this needs to be
// recognizes as an async2 call.
[Intrinsic]
[BypassReadyToRun]
[MethodImpl(MethodImplOptions.NoInlining)]
public static T Await<T>(Task<T> task)
{
TaskAwaiter <T> awaiter = task.GetAwaiter();
ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState;
Continuation? sentinelContinuation = state.SentinelContinuation;
if (sentinelContinuation == null)
state.SentinelContinuation = sentinelContinuation = new Continuation();
state.Notifier = awaiter;
SuspendAsync2(sentinelContinuation);
return awaiter.Result;
} |
public static Task AwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : INotifyCompletion { ... } | ||
public static void AwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : INotifyCompletion { ... } | ||
[MethodImpl(MethodImplOptions.Async)] | ||
public static void UnsafeAwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume the purpose of public AwaitAwaiterFromRuntimeAsync
would be just for custom awaitables?
Previously, in async2 methods, Roslyn would generate a thunk call in cases when the await argument has Task/ValueTask type and is a method invocation.
Otherwise it would emit
{
var awaiter = arg.GetAwaiter();
if (awaiter.IsComplete())
{
UnsafeAwaitAwaiterFromRuntimeAsync(awaiter)
}
awaiter.GetResult()
}
Now awaiting anything that has Task type can be lowered into Await
helper call.
I.E. await obj.taskField
==> Await(obj.taskField)
public static void UnsafeAwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion | ||
|
||
[MethodImpl(MethodImplOptions.Async)] | ||
public static void Await(Task task); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One extremely common use case is await task.ConfigureAwait(false)
- with the current set of methods this would need to use the more verbose (and presumably less efficient) methods above, but could consider adding overloads for these Await
methods that take a ConfigureAwaitOptions
parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, it was discussed a bit above. We definitely will need a way to handle these.
I don't have a strong opinion on this representation over the existing one. I like that the existing one does not have any ambiguities in its handling. Conversely I like that the new one hides away some of the internal details as @VSadov points out above.
There are some precedents to strict pattern matching on IL sequences, like various boxing sequences or array initialization via
I think the implementation would just forward to That intrinsic recognition does seem to complicate things significantly for this representation, however, as we will need |
I'd appreciate everyone's thoughts on this.
This PR replaces the 'method signature' overloading with a new 'Await' method that JIT should treat as an intrinsic. The primary benefit in this case is that it makes the IL valid according to previous versions of the ECMA specification. Ideally, this should cause fewer failures in tooling that examines IL.
I'm still considering something to deal with the type-mismatch in the return type.