Page 50 - MSDN Magazine, November 2019
P. 50
all it is: the developer’s code interspersed with the logic for handling yields and awaits in this manner, plus a “jump table” at the begin- ning of the method that looks at the <>1__state and decides where to go based on it. Much of the remaining complication comes from error handling, as well as from the ability to execute finally blocks as part of DisposeAsync if the enumerator is disposed before it reaches the end, such as if code breaks out of an await foreach loop early.
Finally, there’s the MoveNextAsync method (Figure 4), which is comparatively simple. If the enumerator isn’t in a good state for MoveNextAsync to be called (for example, it’s already been disposed), then it just returns the equivalent of “new ValueTask<bool>(false).” Otherwise, it resets the ManualResetValueTaskSourceCore<bool> for the next iteration and calls (via a runtime helper) the MoveNext method just shown. Then, if the invocation completed synchro- nously, a Boolean indicating whether it successfully moved next or hit the end of the iteration is returned wrapped in a ValueTask<- bool>, and if it’s completing asynchronously, this object is wrapped in the returned ValueTask<bool>.
All of this is, of course, implementation detail and could easily change in the future. In fact, there are several additional optimiza- tions the compiler can employ and hopefully will in future releases. The beauty of this is you get to keep writing the simple code you want in your async iterators and the compiler handles the details; and as the compiler improves, so, too, do your libraries and appli- cations. The same goes for the runtime and supporting libraries. For example, .NET Core 2.1 and 3.0 both saw significant improve- ments in the infrastructure supporting async methods, such that existing async methods just got better, and those improvements accrue to async iterators, as well.
Much Ado About Threading
It can be tempting to think of things that are asynchronous as also being “thread-safe” and then jumping to conclusions based on that, so it’s important to understand what is and what is not safe when working with async enumerables.
It should be evident that it’s fine for one MoveNextAsync call to occur on a different thread from a previous or subsequent MoveNext- Async call; after all, the implementation may await a task and continue execution somewhere else. However, that doesn’t mean MoveNext- Async is “thread-safe”—far from it. On a given async enumerator, MoveNextAsync must never be invoked concurrently, meaning MoveNextAsync shouldn’t be called again on a given enumerator
Figure 4 MoveNextAsync Method
until the previous call to it has completed. Similarly, DisposeAsync on an iterator shouldn’t be invoked while either MoveNextAsync or DisposeAsync on that same enumerator is still in flight.
These rules are easy to follow, and you’d be hard-pressed not to follow them when using await foreach, which naturally follows the rules as part of the code it translates into. However, it’s possible to write slightly different code and find yourself with a problem.
Consider this buggy variant:
IAsyncEnumerable<int> src = ...; IAsyncEnumerator<int> e = src.GetAsyncEnumerator(); try
{
while (await e.MoveNextAsync().TimeoutAfter(30)) // BUG!! Use(e.Current);
}
finally { if (e != null) await e.DisposeAsync(); }
This snippet is using a hypothetical TimeoutAfter method; it doesn’t actually exist in .NET Core 3.0, but imagine that it did or that some- one wrote it as an extension method, with the semantics that if the task on which it’s called hasn’t completed within the specified timeout, it’ll throw an exception. Now consider this in the context of the previous rules: If this timeout were hit, that means the MoveNextAsync was still in flight, but the TimeoutAfter would cause the iterator to resume with an exception, the finally block to be entered, and DisposeAsync to be called on the enumerator that may still have MoveNextAsync in progress. This could end up failing in a variety of ways, or it could end up accidentally succeeding; in any event, stay away from code like that.
What About LINQ?
Language Integrated Query, or LINQ, provides both a set of helper methods for operating on synchronous enumerables and a set of key- words in the language for writing queries that then compile down to these helper methods. .NET Core 3.0 and C# 8 don’t include either of those for asynchronous enumerables. However, the github.com/dotnet/ reactive project includes the System.Linq.Async library, which provides a full set of such extension methods for operating on IAsyncEnumer- able<T>. You can include this library from NuGet in your project, and have access to a wide array of helpful extension methods for operating over IAsyncEnumerable<T> objects.
What’s Next?
C# 8 and .NET Core 3.0 are exciting releases. They include not only the aforementioned language and library support for async enumerables, but also a variety of types that produce or consume them (for example, the System.Threading.Channels.Channel- Reader<T> type provides a ReadAllAsync method that returns an IAsyncEnumerable<T>). However, this really is just the beginning for async enumerables. I expect subsequent releases will see fur- ther support in the libraries, improvements in the compiler, and additional language functionality. On top of that, I expect we’ll see many NuGet libraries for interacting with IAsyncEnumerable<T>, just as we do for IEnumerable<T>. I’m looking forward to seeing allofthewaysthisfeaturesetwillbeputtogreatuse.Enjoy! n
Stephen toub works on .NET at Microsoft. You can find him on GitHub at github.com/stephentoub.
thankS to the following Microsoft technical experts for reviewing this article: Julien Couvreur, Jared Parsons
ValueTask<bool> IAsyncEnumerator<int>.MoveNextAsync() {
if (<>1__state == -2) return default;
<>v__promiseOfValueOrEnd.Reset();
<RangeAsync>d__1 stateMachine = this; <>t__builder.MoveNext(ref stateMachine);
short version = <>v__promiseOfValueOrEnd.Version;
return <>v__promiseOfValueOrEnd.GetStatus(version) == ValueTaskSourceStatus.Succeeded ?
new ValueTask<bool>(<>v__promiseOfValueOrEnd.GetResult(version)) :
new ValueTask<bool>(this, version); }
38 msdn magazine
C#