-
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
Implement async-iterator methods #28218
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
a513600
Implement async iterator methods
jcouv ef6ed11
Update documentation
jcouv 91aa87a
Small IDE fix
jcouv 32e70c8
Fix exception in ExitPointWalker
jcouv 022e59b
PR tweaks
jcouv 08f73d7
Add missing file
jcouv cc9d2d9
Address PR feedback from Omar
jcouv 1a8e43d
Address PR feedback from Andy
jcouv 5b3e874
Don't call builder.SetResult when method ends
jcouv db94855
Integrate with latest decompiler API
jcouv fac7cf5
Address PR feedback
jcouv 2ca599a
Add return statement in exception handling
jcouv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
async-streams (C# 8.0) | ||
---------------------- | ||
|
||
Async-streams are async variants of enumerables, where getting the next element may involve an async operation. They are types that implement `IAsyncEnumerable<T>`. | ||
|
||
```C# | ||
// Those interfaces will ship as part of .NET Core 3 | ||
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(); | ||
} | ||
} | ||
``` | ||
|
||
When you have an async-stream, you can enumerate its items using a special `foreach` statement: `foreach await (var item in asyncStream) { ... }`. | ||
Similarly, if you have an async-disposable, you can use and dispose it with a special `using` statement: `using await (var resource = asyncDisposable) { ... }` | ||
A `using await` statement is just like a `using` statement, but it uses `IAsyncDisposable` instead of `IDisposable`, and `await DisposeAsync()` instead of `Dispose()`. | ||
|
||
The user can implement those interfaces manually, or can take advantage of the compiler generating a state-machine from a user-defined method (called an "async-iterator" method). | ||
An async-iterator method is a method that: | ||
1. is declared `async`, | ||
2. returns an `IAsyncEnumerable<T>` type, | ||
3. uses both `await` expressions and `yield` statements. | ||
|
||
For example: | ||
```C# | ||
async IAsyncEnumerable<int> GetValuesFromServer() | ||
{ | ||
while (true) | ||
{ | ||
IEnumerable<int> batch = await GetNextBatch(); | ||
if (batch == null) yield break; | ||
|
||
foreach (int item in batch) | ||
{ | ||
yield return item; | ||
} | ||
} | ||
} | ||
``` | ||
|
||
PROTOTYPE(async-streams): TODO: async LINQ | ||
|
||
### Detailed design for async `foreach` statement | ||
PROTOTYPE(async-streams): TODO | ||
|
||
```C# | ||
E e = ((C)(x)).GetAsyncEnumerator() | ||
try | ||
{ | ||
while (await e.WaitForNextAsync()) /* outer loop */ | ||
{ | ||
while (true) /* inner loop */ | ||
{ | ||
V v = (V)e.TryGetNext(out bool success); | ||
if (!success) goto outer_loop_continue; | ||
/* loop.Body */ | ||
/* loop.ContinueLabel: */ | ||
} | ||
outer_loop_continue: | ||
} | ||
/* loop.BreakLabel: */ | ||
} | ||
finally { await e.DisposeAsync(); } | ||
``` | ||
|
||
### Detailed design for async-iterator methods | ||
|
||
The state machine for an async-iterator method primarily implements `IAsyncEnumerable<T>` and `IAsyncEnumerator<T>`. | ||
It is similar to a state machine produced for an async method. It contains builder and awaiter fields, used to run the state machine in the background (when an `await` is reached in the async-iterator). | ||
But it contains additional state: | ||
- a promise of a value-or-end, | ||
- a `bool` flag indicating whether the promise is active or not, | ||
- a current yielded value of type `T`. | ||
|
||
The promise of a value-or-end is returned from `WaitForNextAsync`. It can be fulfilled with either: | ||
- `true` (when a value becomes available following background execution of the state machine), | ||
- `false` (if the end is reached), | ||
- an exception. | ||
The promise is implemented as a `ManualResetValueTaskSourceLogic<bool>` (which is a re-usable and allocation-free way of producing and fulfilling `ValueTask<bool>` instances) and its surrounding interfaces on the state machine: `IValueTaskSource<bool>` and `IStrongBox<ManualResetValueTaskSourceLogic<bool>>`. | ||
|
||
Compared to the state machine for a regular async method, the `MoveNext()` for an async-iterator method adds logic: | ||
- to the handling of an `await`, to reset the promise, | ||
- to the handling of exceptions, to set the exception into the promise, if active, or rethrow it otherwise, | ||
- to support handling a `yield return` statement, which saves the current value and fulfill the promise (if active), | ||
- to support handling a `yield break` statement, which resets the promise (if active) and fulfills it with result `false`. | ||
|
||
This is reflected in the implementation, which extends the lowering machinery for async methods to: | ||
1. properly signal through the promise (update in various methods in `AsyncMethodToStateMachineRewriter`, such as `VisitAwaitExpression`) | ||
2. handle `yield return` and `yield break` statements (add methods `VisitYieldReturnStatement` and `VisitYieldBreakStatement` to `AsyncMethodToStateMachineRewriter`), | ||
3. produce additional state and logic for the promise itself (we use `AsyncIteratorRewriter` instead of `AsyncRewriter` to drive the lowering, and produces the other members: `WaitForNextAsync`, `TryGetNext`, `DisposeAsync`, and some members supporting the resettable `ValueTask`, namely `GetResult`, `SetStatus`, `OnCompleted` and `Value.get`). | ||
|
||
The contract of the `MoveNext()` method is that it returns either: | ||
- in completed state, | ||
- leaving the promise inactive (when started with an inactive promise and a value is immediately available), | ||
- with an exception (when started with an inactive promise and an exception is thrown), | ||
- an active promise, which will later be fulfilled (with `true`, `false` or an exception). | ||
|
||
If the promise is active: | ||
- the builder is running the `MoveNext()` logic, | ||
- a call to `WaitForNextAsync` will not move the state machine forward (ie. it won't call `MoveNext()`), | ||
- a call to `TryGetNext` APIs will throw. | ||
|
||
PROTOTYPE(async-streams): The compiler leverages existing BCL types (including some recently added types from the `System.Threading.Tasks.Extensions` NuGet package) in the state machine it generates. But as part of this feature, we may introduce some additional BCL types, so that the state machine can be further simplified and optimized. | ||
|
||
```C# | ||
ValueTask<bool> WaitForNextAsync() | ||
{ | ||
if (State == StateMachineStates.FinishedStateMachine) | ||
{ | ||
return default(ValueTask<bool>); | ||
} | ||
|
||
if (!this._promiseIsActive || this.State == StateMachineStates.NotStartedStateMachine) | ||
{ | ||
var inst = this; | ||
this._builder.Start(ref inst); | ||
} | ||
|
||
return new ValueTask<bool>(this, _valueOrEndPromise.Version); | ||
} | ||
``` | ||
|
||
```C# | ||
T TryGetNext(out bool success) | ||
{ | ||
if (this._promiseIsActive) | ||
{ | ||
if (_valueOrEndPromise.GetStatus(_valueOrEndPromise.Version) == ValueTaskSourceStatus.Pending) throw new Exception(); | ||
_promiseIsActive = false; | ||
} | ||
else | ||
{ | ||
var inst = this; | ||
this._builder.Start(ref inst); | ||
} | ||
|
||
if (_promiseIsActive || State == StateMachineStates.FinishedStateMachine) | ||
{ | ||
success = false; | ||
return default; | ||
} | ||
|
||
success = true; | ||
return _current; | ||
} | ||
``` | ||
|
||
In terms of the lowered code, there are five changes to the `MoveNext()` method of an async-iterator state machine, compared to that of a regular `async` state machines: | ||
- reaching the end of the method | ||
- reaching an `await` | ||
- reaching a `yield return` | ||
- reaching a `yield break` | ||
- handling exceptions | ||
Those changes are described below for information, but they are compiler implementation details which should not be depended on (such generated code is subject to change without notice). | ||
|
||
When we reach the end of the method, we need to fulfill the promise of value-or-end with `false` to signal that the end was reached: | ||
```C# | ||
if (this.promiseIsActive) | ||
{ | ||
this.promiseOfValueOrEnd.SetResult(false); | ||
} | ||
``` | ||
|
||
When we reach an `await` and the awaitable result wasn't already completed, we need to reset the promise if it was inactive: | ||
```C# | ||
if (!this.promiseIsActive) | ||
{ | ||
this.promiseIsActive = true; | ||
this.promiseOfValueOrEnd.Reset(); | ||
} | ||
``` | ||
|
||
When we reach a `yield return`, we save the "current" value and fulfill the promise of value-or-end with `true` to signal that a value is available: | ||
```C# | ||
this.current = expression; | ||
this.state = <next_state>; | ||
if (this._promiseIsActive) | ||
{ | ||
this._valueOrEndPromise.SetResult(true); | ||
} | ||
goto <exit_label>; | ||
<next_state_label>: ; | ||
this.state = finalizeState; | ||
``` | ||
|
||
When we reach a `yield break`, we make sure the promise of value-or-end is initialized/reset and then fulfill it with `false` to signal that the end was reached: | ||
```C# | ||
if (!this.promiseIsActive) | ||
{ | ||
this.promiseIsActive = true; | ||
this.promiseOfValueOrEnd.Reset(); | ||
} | ||
this.promiseOfValueOrEnd.SetResult(false); | ||
return; | ||
``` | ||
|
||
The `MoveNext()` method of the state machine also includes exception handling. | ||
For regular `async` methods, we catch any such exception and pass it on to the caller of the state machine, by setting the exception in the task being awaited by the caller. | ||
For async-iterators, we also catch any such exception and pass it on to the caller of the state machine (`WaitForNextAsync` and `TryGetNext`) via the promise of value-or-end: | ||
|
||
```C# | ||
catch (Exception ex) | ||
{ | ||
this.state = finishedState | ||
if (promiseIsActive) | ||
{ | ||
this.promiseOfValueOrEnd.SetException(ex); | ||
} | ||
else | ||
{ | ||
throw; | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
src/Compilers/CSharp/Portable/Lowering/AsyncRewriter/AsyncIteratorInfo.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using Microsoft.CodeAnalysis.CSharp.Symbols; | ||
|
||
namespace Microsoft.CodeAnalysis.CSharp | ||
{ | ||
/// <summary> | ||
/// Additional information for rewriting an async-iterator. | ||
/// </summary> | ||
internal sealed class AsyncIteratorInfo | ||
{ | ||
// This `ManualResetValueTaskSourceLogic<bool>` struct implements the `IValueTaskSource` logic | ||
internal FieldSymbol PromiseOfValueOrEndField { get; } | ||
|
||
// Spiritual equivalent of `promise != null` | ||
internal FieldSymbol PromiseIsActiveField { get; } | ||
|
||
// Stores the current/yielded value | ||
internal FieldSymbol CurrentField { get; } | ||
|
||
// Method to reset the promise: `void ManualResetValueTaskSourceLogic<T>.Reset()` | ||
internal MethodSymbol ResetMethod { get; } | ||
|
||
// Method to fulfill the promise with a result: `void ManualResetValueTaskSourceLogic<T>.SetResult(T result)` | ||
internal MethodSymbol SetResultMethod { get; } | ||
|
||
// Method to fulfill the promise with an exception: `void ManualResetValueTaskSourceLogic<T>.SetException(Exception error)` | ||
internal MethodSymbol SetExceptionMethod { get; } | ||
|
||
public AsyncIteratorInfo(FieldSymbol promiseOfValueOrEndField, FieldSymbol promiseIsActiveField, FieldSymbol currentField, | ||
MethodSymbol resetMethod, MethodSymbol setResultMethod, MethodSymbol setExceptionMethod) | ||
{ | ||
PromiseOfValueOrEndField = promiseOfValueOrEndField; | ||
PromiseIsActiveField = promiseIsActiveField; | ||
CurrentField = currentField; | ||
ResetMethod = resetMethod; | ||
SetResultMethod = setResultMethod; | ||
SetExceptionMethod = setExceptionMethod; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 mentioned that
throw
means we'll lose locals. I'll add a PROTOTYPE comment to change this to: