Page 49 - MSDN Magazine, November 2019
P. 49
bool> implementation, with every MoveNextAsync that completes synchronously returning a ValueTask<bool> that just wraps a bool, but every MoveNextAsync that completes asynchronously return- ing a ValueTask<bool> that wraps one of these reusable IValueTask- Source<bool> implementations. And the compiler-generated async enumerable object not only doubles as the enumerator and not only doubles as the async state machine object, it also doubles as exactly that IValueTaskSource<bool> implementation. The same applies to DisposeAsync. Most implementations of DisposeAsync will actually complete synchronously, in which case it can just return a default ValueTask. But if it needs to complete asynchronously, the compiler-generated implementation will just return a ValueTask wrapped around this same async enumerable object, which imple- ments IValueTaskSource, as well. So, no matter how many times MoveNextAsync needs to complete asynchronously, and whether or not DisposeAsync completes asynchronously, they don’t incur any additional overhead of allocations, because they just return this instance wrapped in a ValueTask<bool> or ValueTask, respectively.
After the interfaces, you see a bunch of fields. Some of these, like <>3__start and <>3__count, are there to store the arguments passed to the entry point method (you can see them being set to the arguments in the method shown earlier). These values need to be preserved in case the enumerable is enumerated again, which is why you also see start and count fields; those get initialized to their <>3_ counterparts in the GetAsyncEnumerator method and are then the actual fields manipulated when the corresponding “parameters” in the developer’s code in the async iterator are read and written. Other fields, like <i>5__2, represent the “locals” used in the async iterator (in this case, it’s the i iteration variable in the for loop); any “local” that needs to survive across an await boundary is “lifted” to the state machine in this fashion. But arguably the two most inter- esting fields are the <>t__builder and <>v__promiseOfValueOrEnd fields. The former represents the lifetime of the asynchronous exe- cution, and provides the facilities for the async iterator to hook in with the runtime’s support for things like ExecutionContext flow (ensuring that, for example, AsyncLocal<T> values are properly flowed across awaits). The latter is a ManualResetValueTaskSource- Core<T>, a type introduced in .NET Core 3.0 to contain most of the logic necessary to properly implement IValueTaskSource<T> and IValueTaskSource; the compiler-generated class implements the interfaces, but then delegates the implementations of these interface methods to this mutable struct stored in its field:
bool IValueTaskSource<bool>.GetResult(short token) => <>v__promiseOfValueOrEnd.GetResult(token);
void IValueTaskSource<bool>.OnCompleted(
Action<object> continuation, object state,
short token, ValueTaskSourceOnCompletedFlags flags) => <>v__promiseOfValueOrEnd.OnCompleted(continuation, state, token, flags);
ValueTaskSourceStatus IValueTaskSource<bool>.GetStatus(short token) => <>v__promiseOfValueOrEnd.GetStatus(token);
The remainder of the implementation is really about the state machine itself and moving it forward. The code the developer writes in the async iterator is moved into a MoveNext helper method (Figure 3 shows an approximate decompilation of the IL generated by the C# compiler), just as is done for synchronous iterators and async meth- ods. There are three main ways to return out of this void-returning msdnmagazine.com
helper: the code yields a current value, the code awaits something that hasn’t yet completed, or the code reaches the end of the enumer- ation (either successfully or via an unhandled exception). When the code yields a value, it stores that value into the <>2__current field and updates the <>1__state to indicate where the state machine should jump back to the next time MoveNext is invoked. When the code awaits an incomplete awaiter, it similarly sets the <>1__state (to the location of the code that checks the result of the then-completed awaited operation) and uses the <>t__builder to hook up a contin- uation that will cause the implementation to call MoveNext again (at which point it will jump to the location dictated by <>1__state). While the implementation may look complicated, that’s effectively
Figure 3 State Machine Implementation
private void MoveNext() {
try {
TaskAwaiter awaiter; switch (<>1__state) {
default:
if (<>w__disposeMode) goto DONE_ITERATING; <>1__state = -1;
<i>5__2 = 0; // int i = 0;
goto LOOP_CONDITION;
case 0:
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter); <>1__state = -1;
goto DONE_AWAIT;
case -4:
<>1__state = -1;
if (<>w__disposeMode) goto DONE_ITERATING; <i>5__2++; // i++
goto LOOP_CONDITION;
}
LOOP_CONDITION:
// i < count
if (<i>5__2 >= count) goto DONE_ITERATING;
awaiter = Task.Delay(<i>5__2, cancellationToken).GetAwaiter();
if (!awaiter.IsCompleted) // await Task.Delay(i, cancellationToken); {
<>1__state = 0;
<>u__1 = awaiter;
<RangeAsync>d__1 sm = this; <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref sm); return;
}
DONE_AWAIT:
awaiter.GetResult();
<>2__current = start + <i>5__2;
<>1__state = -4;
goto RETURN_TRUE_FROM_MOVENEXTASYNC; // yield return start + i;
DONE_ITERATING:
<>1__state = -2;
<>x__combinedTokens.Dispose(); <>v__promiseOfValueOrEnd.SetResult(result: false); return;
RETURN_TRUE_FROM_MOVENEXTASYNC:
<>v__promiseOfValueOrEnd.SetResult(result: true); }
catch (Exception e) {
<>1__state = -2; <>x__combinedTokens?.Dispose(); <>v__promiseOfValueOrEnd.SetException(e);
} }
November 2019 37