Page 48 - MSDN Magazine, November 2019
P. 48
in which case the body of the async iterator will similarly see cts. Token as its cancellationToken. Why two different ways to do it? Passing the token directly to the method is easier, but it doesn’t work when you’re handed an arbitrary IAsyncEnumerable<T> from some other source but still want to be able to request can- cellation of everything that composes it. In corner-cases, it can also be advantageous to pass the token to GetAsyncEnumerator, as doing so avoids “burning in” the token in the case where the single enumerable will be enumerated multiple times: By passing it to GetAsyncEnumerator, a different token can be passed each time. Of course, it’s also possible that two different tokens end up getting passed into the same iterator, one as an argument to the iterator and one via GetAsyncEnumerator. In that case, the com- piler-generated code handles this by creating a new linked token that will have cancellation requested when either of the two tokens has cancellation requested, and that new “combined” token will be the one the iterator body sees.
Under the Hood of Async Iterators
Async iterators are transformed by the C# compiler into a state machine, enabling the developer to write simple code while the compiler handles all the complicated intricacies to provide an efficient implementation.
Let’s consider the RangeAsync method shown earlier. To begin, the
compiler emits the method the developer wrote, but with the body
replaced by code that sets up and returns the enumerable object:
[AsyncIteratorStateMachine(typeof(<RangeAsync>d__1))]
static IAsyncEnumerable<int> RangeAsync(int start, int count,
[EnumeratorCancellation] CancellationToken cancellationToken = default) => new <RangeAsync>d__1(-2) {
<>3__start = start,
<>3__count = count, <>3__cancellationToken = cancellationToken
};
You can see that the method has the same signature as was written by the developer (except for the async keyword, which, as with the async keyword on async methods prior to C# 8, only affects how the compiler compiles the method and doesn’t actually impact the method’s signature in metadata), and that its sole purpose is to ini- tialize a new instance of the <RangeAsync>d__1 type, which is the compiler-generated IAsyncEnumerable<int> implementation out- lined in Figure 2. For such a “simple” method as the developer wrote in RangeAsync, there’s a lot going on here, so I’ll break it into pieces.
First, note that this type not only implements IAsyncEnumera- ble<int>, but also a bunch of other interfaces. This isn’t necessary from a functionality perspective, but it’s critical from a performance standpoint. The compiler implementation has been designed to keep allocations incredibly low; in fact, no matter how many times an async iterator yields, the most common case is that it incurs at most two allocations of overhead. To start, this is achieved by employing the same trick that synchronous iterators employ: The same object that implements the enumerable is reused as the enumerator as long as no one else is currently using it. That’s why the object also implements IAsyncEnumerator<int>, because the same object typically doubles as both, returning itself from its GetAsyncEnumerator method.
Then there’s the IAsyncStateMachine interface. The supporting APIs in the core libraries and runtime operate over abstract state machines as represented by this interface, so in order to be able to
perform awaits, the class must implement this interface (it could employ a helper type to implement the interface, but that would be more allocation).
Arguably the most interesting interfaces, however, are IValue- TaskSource<bool> and IValueTaskSource, as this gets at the heart of how async enumerables can have so little overhead. When we first designed the async enumerable interfaces, the MoveNext- Async method returned a Task<bool>. The most common case is for the MoveNextAsync method to actually complete its oper- ation synchronously, in which case the runtime would be able to use a cached task object: Every time it completes synchronously to return a true value, the same already-completed-with-a-true-result Task<bool> could be returned, making the synchronously com- pleting case allocation-free. However, .NET Core 2.1 introduced the ability for a ValueTask<T> to be backed not just by a T or by a Task<T>, but also by a new IValueTaskSource<T> interface. This is powerful in that a developer is able to craft an implementation of IValueTaskSource<T> that can be reset and then reused with sub- sequent ValueTask<T>s. (For more information, see bit.ly/2kEyo81.) By having MoveNextAsync return a ValueTask<bool> instead of a Task<bool>, the compiler can create such an IValueTaskSource<-
Figure 2 Compiler-Generated IAsyncEnumerable<T>
[CompilerGenerated]
private sealed class <RangeAsync>d__1 :
IAsyncEnumerable<int>, IAsyncEnumerator<int>, IAsyncStateMachine,
IValueTaskSource<bool>, IValueTaskSource,
{
private CancellationTokenSource <>x__combinedTokens; public CancellationToken <>3__cancellationToken; public int <>3__start;
public int <>3__count;
public AsyncIteratorMethodBuilder <>t__builder;
public ManualResetValueTaskSourceCore<bool> <>v__promiseOfValueOrEnd;
public int <>1__state;
private int <>l__initialThreadId; private bool <>w__disposeMode;
private CancellationToken cancellationToken; private int start;
private int count;
private int <i>5__2;
private TaskAwaiter <>u__1; private int <>2__current;
public <RangeAsync>d__1(int <>1__state) { ... }
IAsyncEnumerator<int> IAsyncEnumerable<int>.GetAsyncEnumerator( Cancellationb cancellationToken) { ... }
ValueTask<bool> IAsyncEnumerator<int>.MoveNextAsync() { ... } int IAsyncEnumerator<int>.Current { get { ... } }
ValueTask IAsyncDisposable.DisposeAsync() { ... } void IAsyncStateMachine.MoveNext() { ... }
ValueTaskSourceStatus IValueTaskSource<bool>.GetStatus(short token) { ... } ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) { ... }
void IValueTaskSource<bool>.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) { ... }
void IValueTaskSource.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) { ... }
bool IValueTaskSource<bool>.GetResult(short token) { ... }
void IValueTaskSource.GetResult(short token) { ... } }
36 msdn magazine
C#