IAsyncEnumerable combines the power of IEnumerable (which lets us "foreach"
through items) with the joys of async code (that we can "await"). Like many
async methods, we can pass in a CancellationToken to short-circuit the
process. But because of the way that we use IAsyncEnumerable, passing in a
cancellation token is a bit different than we are used to.
Short Version:
The "WithCancellation" extension method on IAsyncEnumerable lets us pass in a cancellation token.
Let's take a quick look at how to use IAsyncEnumerable, and then we'll look at
cancellation.
Using IAsyncEnumerable
I started using IAsyncEnumerable when I exploring channels in C#.
Here's an example method that uses "await foreach" with an IAsyncEnumerable
(taken from the
Program.cs file
of this GitHub repository:
https://github.com/jeremybytes/csharp-channels-presentation):
private static async Task ShowData(ChannelReader<Person> reader)
{
await foreach(Person person in reader.ReadAllAsync())
{
DisplayPerson(person);
};
}
The ChannelReader type has a "ReadAllAsync" method that returns an
IAsyncEnumerable. What this means is that someone else can be writing to the
channel while we read from it. The IAsyncEnumerable means that if there is not
another item ready to read, we can wait for it. So, we can pull things off the
channel as they are added, even if there are delays between each item getting
added.
Waiting for items can cause a problem, and that's where the "async" part comes
in. Since this is asynchronous, we can "await" each item. This gives us the
advantages that we are used to with "await", meaning that we are not blocking
processes and threads while we wait.
By using "await foreach" on an IAsyncEnumerable, we get this combined
functionality: getting the next item in the enumeration and waiting
asynchronously if the next item isn't ready yet.
For more information on channels, check the repository for list of articles and a recorded presentation: https://github.com/jeremybytes/csharp-channels-presentation.
The IAsyncEnumerable Interface
Things got a little more interesting when I was building my own classes that implement IAsyncEnumerable. The interface itself only has one method:
GetAsyncEnumerator. An implementation could look something like this (we'll
call this our "Processor" since it will process some custom data for us):
public IAsyncEnumerator<int> GetAsyncEnumerator(
CancellationToken cancellationToken = default)
{
while (...)
{
// interesting async stuff here
yield return nextValue;
}
}
Like with IEnumerable, we can use "yield return" to return the next value from
the enumeration. Unlike IEnumerable, we can also put asynchronous code in
here, whether it's an asynchronous service call or waiting for a record to
finish a complex process. (That goes in the "// interesting async stuff here"
section; we won't look at that today.)
CancellationToken Parameter
An interesting thing about the GetAsyncEnumerable method is that it has a
CancellationToken parameter. Notice that the the CancellationToken has a
"default" value set. This means that the cancellation token is optional. If we do not pass in a token, the code will still work.
If we wanted to check the cancellation token in the code. This could look
something like this:
public async IAsyncEnumerator<int> GetAsyncEnumerator(
CancellationToken cancellationToken = default)
{
while (...)
{
cancellationToken.ThrowIfCancellationRequested();
// interesting stuff here
yield return nextValue;
}
}
Each time through the "while" loop, the cancellation token will be checked. If
cancellation is requested, then this will throw an OperationCanceledException.
Passing a Cancellation Token
The next part is where things get interesting. How do we actually pass a
cancellation token to the IAsyncEnumerable? If we are using the "Processor"
that we created above. That call could look something like this.
await foreach (int currentItem in processor)
{
DisplayItem(currentItem);
}
When we use "foreach", we do not call the "GetAsyncEnumerable" directly. That
also means that we cannot pass in a cancellation token directly.
But there is an extension method available on IAsyncEnumerable that helps us
out: WithCancellation.
Here's that same foreach loop with a cancellation token passed in:
await foreach (int currentItem in processor.WithCancellation(tokenSource.Token))
{
DisplayItem(currentItem);
}
This assumes that we have a CancellationTokenSource (called "tokenSource")
elsewhere in our code.
For more information on Cancellation, you can refer to
Task and Await: Basic Cancellation. The article was written a while back. Updated code samples (.NET 6) are
available here: https://github.com/jeremybytes/using-task-dotnet6.
ConfigureAwait
As a side note, there is another extension method on IAsyncEnumerable:
ConfigureAwait. This lets us use "ConfigureAwait(false)" in areas that we need
it. ConfigureAwait isn't needed in the ASP.NET world anymore, but it can still
be useful if we are doing desktop or other types of programming.
Wrap Up
IAsyncEnumerable gives us some pretty interesting abilities. I've been
exploring it quite a bit lately. In some code comparisons, I was able to move
async code into a library that made using it quite a bit easier. Once that sample
code is ready, I'll be sharing it on GitHub.
Until then, keep exploring. Sometimes the answers are not obvious. It's okay
if it takes some time to figure things out.
Happy Coding!
No comments:
Post a Comment