-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Add initial async iterators support to System.Private.CoreLib #20442
Conversation
|
||
namespace System.Collections.Generic | ||
{ | ||
public interface IAsyncEnumerable<out T> |
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.
IAsyncEnumerable [](start = 21, length = 16)
I don't know what level of xml doc is expected. Consider adding (here and other public types).
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.
Sure, I'll add 'em.
/// <typeparam name="TStateMachine">The type of the state machine.</typeparam> | ||
/// <param name="stateMachine">The state machine instance, passed by reference.</param> | ||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
public void MoveNext<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => |
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.
MoveNext [](start = 20, length = 8)
While I agree that MoveNext
is clearer, I'm now wondering if keeping Start
as a name might be better, to keep consistency with previous builders.
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 understand the desire for consistency, but this is actually one of the things that highlights the conceptual difference between the builders. Start
makes sense for Async{Void/Task/Task<T>}MethodBuilder
because it's invoked once at the beginning of the operation in order to start it, and it's never used again for the duration of the async method's operation. In contrast, this method on AsyncIteratorMethodBuilder
is used every time the compiler needs to push the state machine forward; seems wrong to me to name a different concept the same thing.
A couple of questions/throughts:
|
ValueTask shipped in System.Private.CoreLib / the System.Runtime contract in .NET Core 2.1. It was then also available in the System.Threading.Tasks.Extensions NuGet package downlevel.
The
Since it's just depending on AsyncTaskMethodBuilder today, there shouldn't be an issue... you can update the compiler to depend on AsyncIteratorMethodBuilder once this is merged, and then work with that. After preview1 sounds fine. It's also possible we could say "forget AsyncIteratorMethodBuilder, just use AsyncTaskMethodBuilder"... that can obviously be made to work. It just felt like this was different enough that we should have a dedicated API for it. |
Sounds good to me. Thanks
If |
/// <summary>Provides the core logic for implementing a manual-reset <see cref="IValueTaskSource"/> or <see cref="IValueTaskSource{TResult}"/>.</summary> | ||
/// <typeparam name="TResult"></typeparam> | ||
[StructLayout(LayoutKind.Auto)] | ||
public struct ManualResetValueTaskSourceLogic<TResult> |
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.
ManualResetValueTaskSourceLogic [](start = 18, length = 31)
It looks like MRVTSL no longer needs to be constructed with an IStrongBox<T>
parent.
I'll take a note to update the generate state machine (no longer needs to implement IStrongBox<T>
).
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 there previously to avoid an allocation in two situations:
- When invoking the continuation with an ExecutionContext
- When queueing the continuation to a SynchronizationContext
I addressed the first with an internal helper on ExecutionContext. I decided for the second that it was better to just incur the allocation when a SynchronizationContext was involved (right now it incurs it on each continuation invocation, but we could change that to allocate once and cache), as this generally isn't a scenario that needs to be optimized heavily.
I really disliked my IStrongBox<MRVTSL>
solution here previously because it ended up exposing implementation details out of anything that wanted to wrap an MRVTSL
: anyone could cast to the interface and then extract a reference to this critical private state of the implementation.
CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default)); | ||
} | ||
|
||
return DisposeAsyncCore(); |
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 try / finally
in DisposeAsyncCore
suggests exceptions are possible. Won't it violate the async dispose pattern to have exceptions escape here vs. returned in ValueTask
?
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.
Hmm, I'm not quite following you. No exceptions should escape here: if any occur inside DisposeAsyncCore, they'll be stored into the ValueTask that's returned, which presumably is the behavior we'd want.
I have been debating with myself the larger question about whether exceptions should be valid at all. In general Dispose isn't supposed to throw, and thus DisposeAsync shouldn't either (or return a ValueTask that might fault with an exception), but in cases where an implementation's Dispose is already throwing, it seems reasonable to keep parity on the DisposeAsync, rather than eating the exceptions in cases where they wouldn't be eaten in Dispose.
Opinion?
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.
Ugg ... I missed that DisposeAsyncCore
is async
. I only checked this method for it.
Yeah I agree that no exceptions is a great principal but reality doesn't always line up with it.
public bool RunContinuationsAsynchronously { get; set; } | ||
|
||
/// <summary>Resets to prepare for the next operation.</summary> | ||
public void Reset() |
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.
Reset [](start = 20, length = 5)
Should Reset
guard against calls at certain times (when previous operation isn't completed)?
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.
My current thinking is "no", but I'm happy to be convinced otherwise.
Since this is a "manual reset" type, I think the owner of the type should be fully responsible for deciding when they're ok with calling Reset. There are multiple definitions of whether the previous operation is completed, e.g. whether SetResult/Exception have been called, whether GetResult has been called, etc., but it could also be that the consumer of the type has their own scheme in place. We do validation in the IValueTaskSource-implementation methods, because that's about how this instance is consumed when wrapped in a ValueTask, but I've avoided doing such second-guessing in the methods that the implementer uses directly (namely SetResult/Exception and Reset).
...tem.Private.CoreLib/shared/System/Threading/Tasks/Sources/ManualResetValueTaskSourceLogic.cs
Show resolved
Hide resolved
482f5d1
to
baa1583
Compare
baa1583
to
5b55363
Compare
@dotnet-bot test this please |
@AndyAyersMS, this is hitting an assert in the JIT / crossgen in some of the legs, e.g.
It aborted on several unix legs, which might be due to the same thing?
|
5b55363
to
734e176
Compare
Could be the same issue, but might not be. Let me take a look. |
For the assert: the jit is trying to handle the impact of adding the monitor exit call to a synchronized method that also returns a struct type, and it's not prepared for quite this complicated of a tree: in particular that the source side is a comma.
Only happens selectively because it is ABI dependent. For instance on x64 windows we return the structure via a hidden byref and so returns have a different tree shape. This is a jit bug that we'll have to fix. Let me see if I can work up a simple repro. cc @dotnet/jit-contrib |
Crossgen failure looks to be the same issue (opened as #20499).
|
Thanks, @AndyAyersMS. |
@dotnet-bot test Ubuntu x64 Checked CoreFX Tests please
|
Other PRs are hitting this ubuntu failure too: #20507. |
Thanks. |
@@ -201,6 +203,85 @@ internal static void RunInternal(ExecutionContext executionContext, ContextCallb | |||
edi?.Throw(); | |||
} | |||
|
|||
// Direct copy of the above RunInternal overload, except that it passes the state into the callback strongly-typed and by ref. | |||
internal static void RunInternal<TState>(ExecutionContext executionContext, ContextCallback<TState> callback, ref TState state) |
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.
Should QueueUserWorkItemCallback<TState>
use this overload?
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.
Should QueueUserWorkItemCallback use this overload?
Don't know... it would need to be measured.
@dotnet-bot test Ubuntu x64 Checked CoreFX Tests please |
Commit migrated from dotnet/coreclr@0cfd539
IAsyncEnumerable<T>
,IAsyncEnumerator<T>
, andIAsyncDisposable
interfacesAsyncIteratorMethodBuilder
struct for the compilersIAsyncDisposable
implementations to the types in System.Private.CoreLib that benefit from itManualResetValueTaskSourceLogic
type that the compilers use to implement async iterators.ExecutionContext
andThreadPool
to supportManualResetValueTaskSourceLogic
(these helpers are currently internal but could potentially be public in the future).Contributes to https://github.com/dotnet/corefx/issues/32640
Contributes to https://github.com/dotnet/corefx/issues/32665
Contributes to https://github.com/dotnet/corefx/issues/32664
All of this still needs unit tests and to be exposed in the refs in corefx (this will not be merged until I have that ready and validated locally). There will also be additional work in corefx to complete the above issues, e.g. more Stream-derived types that'll override DisposeAsync. And all of this is still subject to an upcoming API review. I'm putting it up now to get a jump on everything so we can get it merged as soon as possible.
cc: @jcouv, @terrajobst, @jaredpar, @MadsTorgersen, @benaadams, @kouvel