Page 47 - MSDN Magazine, November 2019
P. 47

asynchronous operations. What about using yield return with async and await to author asynchronous enumerables?
The answer to that question comes in C# 8 and .NET Core 3.0.
A Tour Through Async Enumerables
.NET Core 3.0 introduces the new System.Collections.Generic.IA- syncEnumerable<T> and System.Collections.Generic.IAsync- Enumerator<T> interfaces. These interfaces, shown in Figure 1, should look very familiar, as they closely mirror their synchro- nous generic counterparts, and the concepts map directly: IAsyncEnumerable<T> provides a method to get an enumerator (IAsyncEnumerator<T>), which is disposable and which provides two additional members, one for moving forward to the next ele- ment and one for getting the current element. The deviations from the synchronous counterparts should also stand out: “Async” is used as a prefix in the type names and as a suffix in the names of mem- bers that may complete asynchronously; GetAsyncEnumerator accepts an optional CancellationToken; MoveNextAsync returns a ValueTask<bool> instead of bool; and DisposeAsync returns a ValueTask instead of void. (You’ll also notice the lack of a Reset method, which the synchronous counterpart exposes, but which is effectively deprecated.)
C# provides direct support for async enumerables, just as it does with synchronous enumerables, both for consuming and for producing them. To iterate through them, await foreach is used instead of just foreach:
await foreach (int item in RangeAsync(10, 3)) Console.Write(item + " "); // Prints 10 11 12
And, as with the synchronous code, the compiler transforms this into code very much like you’d write manually if using the interfaces directly:
IAsyncEnumerator<int> e = RangeAsync(10, 3).GetAsyncEnumerator(); try
{
while (await e.MoveNextAsync()) Console.Write(e.Current + " "); }
finally { if (e != null) await e.DisposeAsync(); }
To produce an async enumerable, the language supports writ- ing an iterator just as it does for the synchronous case, but with async IAsyncEnumerable<T> instead of IEnumerable<T> in the
Figure 1 New Async Interfaces
signature (as with the synchronous support, the language also allows IAsyncEnumerator<T> to be used as the return type instead of IAsyncEnumerable<T>):
static async IAsyncEnumerable<int> RangeAsync(int start, int count) {
for (int i = 0; i < count; i++) {
await Task.Delay(i); yield return start + i;
}
The optional CancellationToken argument to GetAsyncEnumer- ator is used as a way to request cancellation of the enumerator: At any point during the enumeration, if cancellation is requested, an in-progress or subsequent MoveNextAsync call may be interrupted and throw an OperationCanceledException (or some derived type, like a TaskCanceledException). This begs two questions:
• If the token needs to be passed to GetAsyncEnumerator, but it’s the compiler that’s generating the GetAsyncEnumerator call for my await foreach, how do I pass in a token?
• If the token is passed to GetAsyncEnumerator and it’s the compiler that’s generating the GetAsyncEnumerator imple- mentation for my async iterator method, from where do I get the passed-in token?
The answer to the first question (which I’ll also come back to shortly) is that there’s a WithCancellation extension method for IAsyncEnumerable<T>. It accepts a CancellationToken as an argu- ment, and returns a custom struct type that await foreach binds to via a pattern rather than via the IAsyncEnumerable<T> interface, letting you write code like the following:
await foreach (int item in RangeAsync(10, 3).WithCancellation(token)) Console.Write(item + " ");
This same pattern-based binding is also used to enable a ConfigureAwait method, which can be chained in a fluent design with WithCancellation, as well:
await foreach (int item in RangeAsync(10, 3).WithCancellation(token). ConfigureAwait(false))
Console.Write(item + " ");
The answer to the second question is a new [EnumeratorCan- cellation] attribute. You can add a CancellationToken parameter to your async iterator method and annotate it with this attribute. In doing so, the compiler will generate code that will cause the token passed to GetAsyncEnumerator to be visible to the body of the async iterator as that argument:
static async IAsyncEnumerable<int> RangeAsync(
int start, int count,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int i = 0; i < count; i++) {
await Task.Delay(i, cancellationToken);
yield return start + i; }
}
This means that if you write this code:
var cts = new CancellationTokenSource();
await foreach (int item in RangeAsync(10, 3).WithCancellation(cts.Token) { ... }
the code inside of RangeAsync will see its cancellationToken parameter equal to cts.Token. Of course, because the token is a normal parameter to the iterator method, it’s also possible to pass the token directly as an argument:
var cts = new CancellationTokenSource();
await foreach (int item in RangeAsync(10, 3, cts.Token) { ... }
}
namespace System.Collections.Generic {
public interface IAsyncEnumerable<out T> {
IAsyncEnumerator<T> GetAsyncEnumerator( CancellationToken cancellationToken = default);
}
public interface IAsyncEnumerator<out T> : IAsyncDisposable {
ValueTask<bool> MoveNextAsync();
T Current { get; } }
}
namespace System {
public interface IAsyncDisposable {
ValueTask DisposeAsync(); }
}
msdnmagazine.com
November 2019 35


































































































   45   46   47   48   49