-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
We're at a point where we should start exposing the necessary library support for C# 8 async streams, aka async enumerables, aka async iterators. While we may find ourselves wanting to make further changes to these APIs, we've locked on what we plan to deliver initially, and we can adapt as necessary based on feedback.
namespace System
{
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
}
namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator();
}
public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
ValueTask<bool> MoveNextAsync();
T Current { get; }
}
}
namespace System.Runtime.CompilerServices
{
public struct AsyncIteratorMethodBuilder
{
public static AsyncIteratorMethodBuilder Create();
public void MoveNext<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine;
public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine;
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine;
public void Complete();
}
}Assembly:
We need to decide where we put these. My expectation is .NET Core we'd have all of this in System.Private.CoreLib, but we could also put it in System.Threading.Tasks.Extensions a new assembly. If we want to OOB this, it could be in both, with the .NET Core S.T.T.Extensions the new assembly just type forwarding to S.P.CoreLib as happens today for ValueTask.
A few notes:
- Alternative
WaitForNextAsync+TryGetNextcurrent design. We spent a lot of time exploring this attractive alternative. It has perf advantages, but also non-trivial additional complexity when using the APIs directly rather than through compiler support (e.g.foreach await). We also have a design where we could light-up with the alternative API should we find that the perf benefits are desirable enough to introduce a second interface. As such, we're going with the simpler, more familiar, and easier to work withMoveNextAsync+Currentdesign. - Thread safety. While these APIs are for asynchrony, that doesn't imply an instance of an enumerator can be used concurrently. Async enumerators are explicitly not thread safe, in terms of there being no way to correctly used
MoveNextAsync+Currentin an atomic fashion (without higher-level locking that provided the thread safety), but also in terms of accessingMoveNextAsyncagain before having consumed a previous callsValueTask; doing so is erroneous and has undefined behavior. So, too, is accessingDisposeAsyncafter having calledMoveNextAsyncbut without having consumed itsValueTask. ValueTaskinstead ofTask. The original design called forMoveNextAsyncto returnTask<bool>andDisposeAsyncto returnTask. That works well whenMoveNextAsyncandDisposeAsynccomplete synchronously, but when they complete asynchronously, it requires allocation of another object to represent the eventual completion of the async operation. By instead returningValueTask<bool>andValueTask, an implementation can choose to implementIValueTaskSourceand reuse the same object repeatedly for one call after another; in this fashion, for example, the compiler-generated type that's returned from an iterator can serve as the enumerable, as the enumerator, and as the promise for every asynchronously-completingMoveNextAsyncandDisposeAsynccall made on that enumerator, such that the whole async enumerable mechanism can incur overhead of a single allocation.- Cancellation. Cancellation is provided external to the interfaces. If you want operations on the enumerable to be cancelable, you need to pass a
CancellationTokento the thing creating the enumerable, such that the token can be embedded into the enumerable and used in its operation. You can of course choose to cancel awaits by awaiting something that itself represents both theMoveNextAsyncValueTask<bool>and aCancellationToken, but that would only cancel the await, not the underlying operation being awaited. As for IAsyncDisposable, while in theory it makes sense that anything async can be canceled, disposal is about cleanup, closing things out, freeing resources, etc., which is generally not something that should be canceled; cleanup is still important for work that's canceled. The same CancellationToken that caused the actual work to be canceled would typically be the same token passed to DisposeAsync, making DisposeAsync worthless because cancellation of the work would cause DisposeAsync to be a nop. If someone wants to avoid being blocked waiting for disposal, they can avoid waiting on the resulting ValueTask, or wait on it only for some period of time. - Naming.
Asyncis used in the various method names (e.g.DisposeAsync) even though the type also includesAsyncso that a type might implement both the synchronous and asynchronous counterparts and easily differentiate them. AsyncIteratorMethodBuilder. The compiler could get away with using the existingAsyncTaskMethodBuilderorAsyncVoidMethodBuildertypes, but these both have negative impact that can be avoided by using a new, specially-designed type.AsyncTaskMethodBuilderallocates aTaskto represent the async method, but thatTaskgoes unused in an iterator.AsyncVoidMethodBuilderinteracts withSynchronizationContext, becauseasync voidmethods need to do so (e.g. callingOperationStartedandOperationCompletedon the currentSynchronizationContextif there is one). And some of the methods are poorly named, e.g.Startmakes sense when talking about starting an async method, but not when talking about iterating with an iterator. As such, we introduce a new type tailored to async iterators.Createjust returns a builder that the compiler can use, which is likely justdefault(AsyncIteratorMethodBuilder), but the method might also do additional optional work, like tracing.MoveNextpushes the state machine forward, effectively just callingstateMachine.MoveNext(), but doing so with the appropriate handling ofExecutionContext.AwaitOnCompletedandAwaitUnsafeOnCompletedare exactly what they are on the existing builders. AndCompletejust serves to notify the builder that the iterator has finished iterating: technically this isn't necessary, and it may just be a nop, but it gives us a hook to be able to do things like tracing/logging should we choose to do so. (We could decide not to include this.)- Implementing sync and async interfaces. It's fine for a single type to implement both the sync and async counterparts, e.g.
IEnumerable<T>andIAsyncEnumerable<T>. When consuming manually, the developer can easily distinguish which is being used based on naming (e.g.GetEnumeratorvsGetAsyncEnumerator), and when consuming via the compiler, the compiler will provide syntax for differentiation (e.g.foreachvsforeach await).
What else will we want?
I've opened several additional issues to cover related support we'll want to consider:
- Extensions for ConfigureAwait (https://github.com/dotnet/corefx/issues/32684). The compiler will support pattern-based
foreach awaitin addition to binding to the interface, and we can use that to enable the implicit awaits onMoveNextAsyncandDisposeAsyncto be done usingConfigureAwait(false)by having an extension method likepublic static ConfiguredAsyncEnumerable<T> ConfigureAwait<T>(this IAsyncEnumerable<T> enumerable, bool continueOnCapturedContext), where the returnedConfiguredAsyncEnumerable<T>will propagate that through to aConfiguredAsyncEnumerator<T>, and itsMoveNextAsyncandDisposeAsyncwill returnConfiguredValueTaskAwaitables. - ManualResetValueTaskSource (https://github.com/dotnet/corefx/issues/32664). If
MoveNextAsyncandDisposeAsyncwere defined to returnTasks, then the compiler could useTaskCompletionSource<T>to implement those async operations. But as it's returningValueTask, and as we're doing so to enable object reuse, the compiler will be using its own implementation ofIValueTaskSource. To greatly simplify that and to encapsulate all of the relevant logic, we should productize theManualResetValueTaskSource/ManualResetValueTaskSourceLogichelper type from https://github.com/dotnet/corefx/blob/master/src/Common/tests/System/Threading/Tasks/Sources/ManualResetValueTaskSource.cs. - IAsyncDisposable implementations (https://github.com/dotnet/corefx/issues/32665). With
IAsyncDisposableexposed, we'll want to implement on a variety of types in coreclr/corefx where the type could benefit from having an asynchronous disposable capability in addition to an existing synchronous ability.
cc: @jcouv, @MadsTorgersen, @jaredpar, @terrajobst, @tarekgh, @kouvel