Monday, December 20, 2021

Cancelling IAsyncEnumerable in C#

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