Skip to content
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

Merged
merged 4 commits into from
Nov 18, 2018
Merged

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Nov 11, 2018

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:

	[CompilerGenerated]
	private sealed class <M>d__0<T> : IAsyncEnumerable<T>, IAsyncEnumerator<T>, IAsyncDisposable, IValueTaskSource<bool>, IStrongBox<ManualResetValueTaskSourceLogic<bool>>, IAsyncStateMachine
	{
...
		[DebuggerHidden]
		ValueTask<bool> IAsyncEnumerator<T>.MoveNextAsync()
		{
			if (<>1__state == -2)
			{
				return default(ValueTask<bool>);
			}
			<>v__promiseOfValueOrEnd.Reset();
			<M>d__0<T> stateMachine = this;
			<>t__builder.Start(ref stateMachine);
			return new ValueTask<bool>(this, <>v__promiseOfValueOrEnd.Version);
		}

		[DebuggerHidden]
		IAsyncEnumerator<T> IAsyncEnumerable<T>.GetAsyncEnumerator()
		{
			return this;
		}
	}

	[AsyncStateMachine(typeof(<M>d__0<>))]
	[IteratorStateMachine(typeof(<M>d__0<>))]
	private static IAsyncEnumerable<T> M<T>(T value)
	{
		<M>d__0<T> <M>d__ = new <M>d__0<T>();
		<M>d__.value = value;
		<M>d__.<>t__builder = AsyncVoidMethodBuilder.Create();
		<M>d__.<>1__state = -1;
		<M>d__.<>v__promiseOfValueOrEnd = new ManualResetValueTaskSourceLogic<bool>(<M>d__);
		return <M>d__;
	}

After:

	[CompilerGenerated]
	private sealed class <M>d__0<T> : IAsyncEnumerable<T>, IAsyncEnumerator<T>, IAsyncDisposable, IValueTaskSource<bool>, IStrongBox<ManualResetValueTaskSourceLogic<bool>>, IAsyncStateMachine
	{
...
		[DebuggerHidden]
		public <M>d__0(int <>1__state)
		{
			this.<>1__state = <>1__state;
			<>l__initialThreadId = Environment.CurrentManagedThreadId;
			<>t__builder = AsyncVoidMethodBuilder.Create();
			<>v__promiseOfValueOrEnd = new ManualResetValueTaskSourceLogic<bool>(this);
		}

		[DebuggerHidden]
		IAsyncEnumerator<T> IAsyncEnumerable<T>.GetAsyncEnumerator()
		{
			<M>d__0<T> <M>d__;
			if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
			{
				<>1__state = 0;
				<M>d__ = this;
			}
			else
			{
				<M>d__ = new <M>d__0<T>(0); // make a new enumerator instance if needed
			}
			<M>d__.value = <>3__value; // copy parameters
			return <M>d__;
		}
	}


	[AsyncStateMachine(typeof(<M>d__0<>))]
	[IteratorStateMachine(typeof(<M>d__0<>))]
	private static IAsyncEnumerable<T> M<T>(T value)
	{
		<M>d__0<T> <M>d__ = new <M>d__0<T>(-2);
		<M>d__.<>3__value = value; // save parameters
		return <M>d__;
	}

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");
}
Copy link
Member

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();

Copy link
Member Author

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

Copy link
Member

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.

@jcouv
Copy link
Member Author

jcouv commented Nov 13, 2018

@dotnet/roslyn-compiler for a second review. Thanks

@jcouv
Copy link
Member Author

jcouv commented Nov 14, 2018

@dotnet/roslyn-compiler for second review. Thanks

@jasonmalinowski jasonmalinowski changed the base branch from dev16.0-preview2 to master November 16, 2018 19:40
Copy link
Member

@agocke agocke left a 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();
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

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");
}
Copy link
Member

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.

@jcouv jcouv merged commit 71e1473 into dotnet:master Nov 18, 2018
@jcouv jcouv deleted the thread-id branch November 18, 2018 02:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants