-
Notifications
You must be signed in to change notification settings - Fork 4k
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
[Umbrella] Work items and test plan for async streams #24037
Comments
Notes from chat with Stephen: The promise should not just be a TaskCompletionSource. It should be possible to make a re-usable
It is possible that .NET Core 2.2 would introduce some reusable type(s) for abstracting this sort of stuff. Then we may not need to inline all the extra fields and low-level logic. We’ll have a base type, which common machinery (as both our prototypes did). We should discuss how this type will arrive in the compilation (always generate, or generate if not referenced, or always use reference?). For ConfigureAwait, we could have an extension For CancellationToken, we could just treat the token as a regular local, captured and available to use in the user code. For We discussed trade-off between WaitForNextAsync/TryGetNext vs. MoveNextAsync/Current patterns. |
Notes on testing the feature in dev hive:
namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator();
}
public interface IAsyncEnumerator<out T> : System.IAsyncDisposable
{
System.Threading.Tasks.ValueTask<bool> WaitForNextAsync();
T TryGetNext(out bool success);
}
}
namespace System
{
public interface IAsyncDisposable
{
System.Threading.Tasks.ValueTask DisposeAsync();
}
}
namespace System.Runtime.CompilerServices
{
public interface IStrongBox<T>
{
ref T Value { get; }
}
}
namespace System.Threading.Tasks
{
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks.Sources;
public struct ManualResetValueTaskSourceLogic<TResult>
{
private static readonly Action<object> s_sentinel = new Action<object>(s => throw new InvalidOperationException());
private readonly IStrongBox<ManualResetValueTaskSourceLogic<TResult>> _parent;
private Action<object> _continuation;
private object _continuationState;
private object _capturedContext;
private ExecutionContext _executionContext;
private bool _completed;
private TResult _result;
private ExceptionDispatchInfo _error;
private short _version;
public ManualResetValueTaskSourceLogic(IStrongBox<ManualResetValueTaskSourceLogic<TResult>> parent)
{
_parent = parent ?? throw new ArgumentNullException(nameof(parent));
_continuation = null;
_continuationState = null;
_capturedContext = null;
_executionContext = null;
_completed = false;
_result = default;
_error = null;
_version = 0;
}
public short Version => _version;
private void ValidateToken(short token)
{
if (token != _version)
{
throw new InvalidOperationException();
}
}
public ValueTaskSourceStatus GetStatus(short token)
{
ValidateToken(token);
return
!_completed ? ValueTaskSourceStatus.Pending :
_error == null ? ValueTaskSourceStatus.Succeeded :
_error.SourceException is OperationCanceledException ? ValueTaskSourceStatus.Canceled :
ValueTaskSourceStatus.Faulted;
}
public TResult GetResult(short token)
{
ValidateToken(token);
if (!_completed)
{
throw new InvalidOperationException();
}
_error?.Throw();
return _result;
}
public void Reset()
{
_version++;
_completed = false;
_continuation = null;
_continuationState = null;
_result = default;
_error = null;
_executionContext = null;
_capturedContext = null;
}
public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
{
if (continuation == null)
{
throw new ArgumentNullException(nameof(continuation));
}
ValidateToken(token);
if ((flags & ValueTaskSourceOnCompletedFlags.FlowExecutionContext) != 0)
{
_executionContext = ExecutionContext.Capture();
}
if ((flags & ValueTaskSourceOnCompletedFlags.UseSchedulingContext) != 0)
{
SynchronizationContext sc = SynchronizationContext.Current;
if (sc != null && sc.GetType() != typeof(SynchronizationContext))
{
_capturedContext = sc;
}
else
{
TaskScheduler ts = TaskScheduler.Current;
if (ts != TaskScheduler.Default)
{
_capturedContext = ts;
}
}
}
_continuationState = state;
if (Interlocked.CompareExchange(ref _continuation, continuation, null) != null)
{
_executionContext = null;
object cc = _capturedContext;
_capturedContext = null;
switch (cc)
{
case null:
Task.Factory.StartNew(continuation, state, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
break;
case SynchronizationContext sc:
sc.Post(s =>
{
var tuple = (Tuple<Action<object>, object>)s;
tuple.Item1(tuple.Item2);
}, Tuple.Create(continuation, state));
break;
case TaskScheduler ts:
Task.Factory.StartNew(continuation, state, CancellationToken.None, TaskCreationOptions.DenyChildAttach, ts);
break;
}
}
}
public void SetResult(TResult result)
{
_result = result;
SignalCompletion();
}
public void SetException(Exception error)
{
_error = ExceptionDispatchInfo.Capture(error);
SignalCompletion();
}
private void SignalCompletion()
{
if (_completed)
{
throw new InvalidOperationException();
}
_completed = true;
if (Interlocked.CompareExchange(ref _continuation, s_sentinel, null) != null)
{
if (_executionContext != null)
{
ExecutionContext.Run(
_executionContext,
s => ((IStrongBox<ManualResetValueTaskSourceLogic<TResult>>)s).Value.InvokeContinuation(),
_parent ?? throw new InvalidOperationException());
}
else
{
InvokeContinuation();
}
}
}
private void InvokeContinuation()
{
object cc = _capturedContext;
_capturedContext = null;
switch (cc)
{
case null:
_continuation(_continuationState);
break;
case SynchronizationContext sc:
sc.Post(s =>
{
ref ManualResetValueTaskSourceLogic<TResult> logicRef = ref ((IStrongBox<ManualResetValueTaskSourceLogic<TResult>>)s).Value;
logicRef._continuation(logicRef._continuationState);
}, _parent ?? throw new InvalidOperationException());
break;
case TaskScheduler ts:
Task.Factory.StartNew(_continuation, _continuationState, CancellationToken.None, TaskCreationOptions.DenyChildAttach, ts);
break;
}
}
}
} |
Some notes on WIP design: For DisposeAsync implementation, we need to be able to jump to a specific Update:
To design is comprised of four parts:
For: try
{
try { ... }
finally { }
...
try { ... }
finally { }
...
}
finally { } We adjust the lowering logic like this: tryEntry1:
try
{
if (disposeMode) { /* route based on state to tryEntry2 or tryEntry3 */ }
tryEntry2: /*part 1*/
try
{
if (disposeMode) goto finallyEntry2; // simplified routing /*part 2*/
...
finallyEntry2: /*part 3*/
}
finally { }
if (disposeMode) goto finallyEntry1; /*part 4*/
...
tryEntry3:
try
{
if (disposeMode) goto finallyEntry3; // simplified routing
...
finallyEntry3:
}
finally { }
if (disposeMode) goto finallyEntry1;
...
finallyEntry1:
}
finally { } Then Note: only |
Individual issues have been open for follow-ups (optimizations and improved diagnostics) |
Spec: https://github.com/dotnet/csharplang/blob/master/proposals/csharp-8.0/async-streams.md
Championed issue: dotnet/csharplang#43
Notes on cancellation token and
[EnumeratorCancellation]
: http://blog.monstuff.com/archives/2019/03/async-enumerables-with-cancellation.htmlFAQ and known issues
The preview1 compiler is looking for that type, but .NET Core 3 preview 1 contains
ManualResetValueTaskSourceCore
. The solution is to include this code snippet in your program. This will be fixed in preview2.yield return
in construct withfinally
) (see issue Async-Streams: iteration stops early on Core #31268)NullableReferenceTypes
project property doesn't take full effect in legacy projects yet (Add support for Nullable build setting project-system#4058)Async-iterator methods
blockusing
declarations, including asynchronous ones #32589 forawait using
declarations)yield return
when directly following anotheryield return
(to reduce cost inManualResetValueTaskSourceCore.SetResult
) (issue Async-streams: Consider optimizing handling of yield directly following yield #31248, needs LDM)GetStatus
andOnCompleted
for implementingIValueTaskSource
andIValueTaskSource<bool>
Async-streams: consider factoring IValueTaskSource implementation methods #31517goto
,continue
,break
async
Async-streams: improve diagnostic when missingasync
keyword #31113IAsyncEnumerable<T>
andIAsyncEnumerator<T>
.factor some code into a base type?(no)introduce new exception type?(not needed at the moment)EnumeratorCancellation
issue Support forEnumeratorCancellationAttribute
(C# 8.0) mono/mono#14454)DisposeAsync()
: if state is-1
, then throw. PR Add guard to DisposeAsync #31764MoveNextAsync()
if possible (issue Async-streams: Consider optimizing return logic ofMoveNextAsync()
#31246, PR Async-streams: Optimize return of MoveNextAsync #31645)GetAsyncEnumerator
API (PR Async-streams: Add CancellationToken to GetAsyncEnumerator #31624)finally
issue DisposeAsync of async-iterator method should execute requiredfinally
blocks #30260, PR Async-streams: Disposal in async-iterator methods #31527DisposeAsync()
. it is ok to dispose before enumerating, and then you start enumerating?AsyncIteratorStateMachineAttribute
PR Async-streams: Emit AsyncIteratorStateMachine attribute on async-iterator methods #31553AwaitOnCompleted
instead ofAwaitUnsafeOnCompleted
AsyncIteratorMethodBuilder
,ManualResetValueTaskSourceCore
, removeIStrongBox<T>
) PR Async-streams: use ManualResetValueTaskSourceCore and AsyncIteratorMethodBuilder types #31330IAsyncEnumerator<T>
Async-streams: Add support for enumerator-returning async-iterator method #31057, PR Async-iterator method can returnIAsyncEnumerator<T>
#31114threadID
design (see https://github.com/dotnet/corefx/issues/3481)GetAsyncEnumerator()
method should make a new instance in some cases Async-Streams: GetAsyncEnumerator should produce new instances when needed #30275, PR Allow multiple calls to GetAsyncEnumerator #31105IOperation
andCFG
should not crash Async-streams: minimal test for IOperation and CFG. Improve diagnostics #30363IAsyncEnumerable
API Update IAsyncEnumerator API #30280Async using and foreach
ref struct
enumerator #32794)Dispose
extension methods should not even be considered in pattern-based disposal #32767)await foreach
to use extension methods #32289)ERR_AwaitForEachMissingMemberWrongAsync
GetAwaitExpressionInfo
SatisfiesForEachPattern
(special method of looking upCurrent
)IEnumerable<T>
andIAsyncEnumerable<T>
.IDisposable
andIAsyncDisposable
.await foreach
andawait using
(issue Async-streams:await foreach
doesn't dispose result fromConfigureAwait
#32316)ConfigureAwait
/WithCancellation
not recognized byawait foreach
(issueConfiguredAsyncEnumerable
in netcoreapp3.0 preview is not acceptable with await foreach (CS8142)? #31609, PR Async-streams: Recognize MoveNextAsync that returns an awaitable #32233)CancellationToken
(issue Adjust pattern-based lookup forawait foreach
#32111, PR Pattern-based 'await foreach' should find parameterless 'GetAsyncEnumerator' and 'MoveNextAsync' #32184)IOperation
andCFG
should not crash Async-streams: minimal test for IOperation and CFG. Improve diagnostics #30363await using
andawait foreach
syntax Async-streams: Update syntax to 'await using' and 'await foreach' #30525IAsyncEnumerable
API Update IAsyncEnumerator API #30280Only convert/deconstruct whenTryGetNext
succeeded Convert/deconstruct in async-foreach only if TryGetNext succeeded #30258IAsyncEnumerator.WaitForNextAsync
to return aValueTask<bool>
.WaitForNextAsync
foreach await (ref x in ...) ...
(error,TestWithPattern_Ref
)WaitForNextAsync
that returns a task-like.Productivity (code fixers/refactorings/etc):
ExtractMethod doesn't work on parts of or entire async-iterator method body(that's expected)Dispose
referenced inusing
andforeach
statements #28228)GetAwaiter
from an async-foreach or async-dispose.DisposeAsync
from an async-foreach or async-dispose.AsyncDispose
methoddecompilation feature in IDE(we'll let ILSpy library fix this)foreach
(by adding/removingawait
) depending on the collection type. (PR Add MakeStatementAsynchronous fixer #33463)using
(by adding/removingawait
) depending on resource type. (PR Add MakeStatementAsynchronous fixer #33463)yield
orawait
(MakeMethodAsync PR MakeMethodAsync: fix iterator methods #31846)foreach await
in non-async method should offer to convert the method toasync
Adding some IDE code fix tests for async using & foreach #26632await using
andawait foreach
. For instance, seeIsLocalVariableDeclarationContext
.async
andawait
(see Fixing async keyword highlighting on local functions #25037, PR Adding await keyword highlighting on async using & foreach #25056) (verified manually on method and local function)ConvertForeachToFor
refactoring (verified manually, not triggered)LDM open issues:
return from e in async-collection select await e + 1; // await isn't a keyword
return from e in async-collection select e + 1;
lower into LINQ APIs? (the one withawait
involvesTask<int>
and the other one involvesint
directly) How many overloads ofSelect
do we need?GetAsyncEnumerator
or inMoveNextAsync
? (if we do, we need to store the token and maybe dispose it too) (answer: no)params
, or for variable name, likevalue
) for token (answer: we're not going to use a keyword)await using
should recognize aDisposeAsync
method that returns a task-like (or onlyValueTask
)? (not applicable because no ref structs in async methods)foreach
recognize... GetAsyncEnumerator()
(withoutCancellationToken
)? (yes, LDM 1/9)AsyncIteratorStateMachineAttribute
)struct
async enumerator? (no, same as regular async, seeTestWithPattern_WithStruct_MoveNextAsyncReturnsTask
)DisposeAsync
return a non-genericValueTask
, since there is now one? Or stick withTask
?GetAsyncEnumerator
,WaitForNextAsync
, andTryGetNext
do not contribute. This mirrors behavior for regularforeach
. But I'd like to confirm. (answer: this is probably something we want to support. Let's queue that for later in the implementation)Task<bool>
? (answer: yes)dynamic
since there is no async counterpart to the non-genericIEnumerable
that would convertdynamic
to. (answer: seems ok)async
keyword on async iterator methods? I assume yes.yield
orawait
, should warn? (answer: withoutyield
it's not recognized as an iterator, warn when noawait
)TryGetNext
first, then checkingsuccess
and only then dealing with conversions and deconstructions.Championed issue: dotnet/csharplang#43 (includes LDM notes)
Test ideas for async foreach:
Test ideas for async using:
Test ideas for async iterators:
GetIteratorElementType
withIsDirectlyInIterator
that relates to speculation, needs testingBindYieldBreakStatement
); same for yield return (seeBindYieldReturnStatement
)BindYieldReturnStatement
) validates escape rules, needs testingIAsyncEnumerable<dynamic>
dynamic
?AwaitOnCompleted
andAwaitUnsafeOnCompleted
async IAsyncEnumerable<int> M() { return TaskLike(); }
IAsyncEnumerable<T>
special from the start? Making mark it with an attribute like we did for task-like?BCL (Core)
DefaultCancellationAttribute
type: https://github.com/dotnet/corefx/issues/37012PRs Add initial async iterators support to System.Private.CoreLib coreclr#20442 and Expose async iterator-related types corefx#33104
GetAsyncEnumerator
APIWithCancellation(...)
to callGetAsyncEnumerator
and wrap the result into anIAsyncEnumerable
CancellationToken
toGetAsyncEnumerator()
PR Add CancellationToken parameter to GetAsyncEnumerator coreclr#21397, documentation https://github.com/dotnet/corefx/issues/33338#issuecomment-444728011WithCancellation
design https://github.com/dotnet/corefx/issues/33909IAsyncDisposable.ConfigureAwait
(PRs Implement IAsyncDisposable.ConfigureAwait coreclr#22160 and Expose/test IAsyncDisposable.ConfigureAwait corefx#34783)BCL (mono)
BCL (package)
EnumeratorCancellationAttribute
(PR Add [EnumeratorCancellation] to Microsoft.Bcl.AsyncInterfaces corefx#37719)References:
The text was updated successfully, but these errors were encountered: