-
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
Allow multiple calls to GetAsyncEnumerator #31105
Conversation
3cebdd3
to
c7dc7f6
Compare
var comp = CreateCompilationWithTasksExtensions(new[] { source, s_common }, options: TestOptions.DebugExe); | ||
comp.VerifyDiagnostics(); | ||
CompileAndVerify(comp, expectedOutput: "1 2 Stream1:3 4 2 1 2 Stream2:3 4 2 Done"); | ||
} |
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.
} [](start = 7, length = 2)
Perhaps test overlapping enumerators with calls to each outside the lifetime of the other.
var enumerator1 = enumerable.GetAsyncEnumerator();
await enumerator1.MoveNext();
var enumerator2 = enumerable.GetAsyncEnumerator();
await enumerator2.MoveNext();
await enumerator1.DisposeAsync();
await enumerator2.MoveNext();
await enumerator2.DisposeAsync();
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.
Good idea. I'll add
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.
Agreed, especially with an await in between.
@dotnet/roslyn-compiler for a second review. Thanks |
@dotnet/roslyn-compiler for second review. Thanks |
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.
LGTM, once the new test is added.
bodyBuilder.Add(F.BaseInitialization()); | ||
bodyBuilder.Add(F.Assignment(F.Field(F.This(), stateField), F.Parameter(F.CurrentFunction.Parameters[0]))); // this.state = state; | ||
|
||
var managedThreadId = MakeCurrentThreadId(); |
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.
As I mentioned offline, I don't really understand our use of thread id here.
The other thing to consider: if our goal is to reuse the iterators on subsequent calls, a common pattern is probably going to be,
getenumerator && foreach
await
getenumerator && foreach
await
...
During each of those awaits we could be shoved onto the thread pool and come back on a different thread, but in the same task. In those cases it seems like what we would want is not the thread ID, but an async local marking whether or not it's OK to come back.
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 added a note to workitems list to confirm the whole threadID design, to re-convince ourselves that it is sound.
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.
@agocke It turns out I had looked into the threadID question at some point already.
https://github.com/dotnet/corefx/issues/3481 discusses the trade-offs of this design for iterators and concluded to keep the threadID design. The same analysis applies to async-iterators.
var comp = CreateCompilationWithTasksExtensions(new[] { source, s_common }, options: TestOptions.DebugExe); | ||
comp.VerifyDiagnostics(); | ||
CompileAndVerify(comp, expectedOutput: "1 2 Stream1:3 4 2 1 2 Stream2:3 4 2 Done"); | ||
} |
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.
Agreed, especially with an await in between.
The state machine for iterator methods is meant to be used as an enumerable which can produce multiple enumerators, but it also acts as the first enumerator that is returned.
The state machine for async methods is meant to be used only once. The state machine for async-iterator initially followed that design, but we're changing that.
With this PR, the setup of an async-iterator method becomes more like that of an iterator method than that of an async method.
Every time you call
GetAsyncEnumerator()
, you get a fresh enumerator (either by recycling one that is finished, or by instantiate and initializing a new one.This implies that the state machine for async-iterator methods must keep a copy of the original parameters.
Since this design is very much like iterator methods, I have refactored some of the code from
IteratorRewriter
as helper methods and re-used them.Fixes #30275
Before:
After: