Curiosity is bliss    Archive    Feed    About    Search

Julien Couvreur's programming blog and more

Async Enumerables with Cancellation

In this post, I’ll explain how to produce and consume async enumerables with support for cancellation.

Let’s start with some context. C# 8.0 added support for async-streams, which is composed of three parts:

  1. async-iterator methods: you can write methods with the async modifier, returning either IAsyncEnumerable or IAsyncEnumerator, and using both yield and await syntax.
  2. await foreach: you can asynchronously enumerate collections that implement IAsyncEnumerable (or implement equivalent APIs).
  3. await using: you can asynchronously dispose resources that implement IAsyncDisposable.

await foreach

await foreach follows a similar execution pattern as its synchronous sibling foreach: it first gets an enumerator from the enumerable (by calling GetAsyncEnumerator(), then repeatedly does await MoveNextAsync() on the enumerator and gets the item with Current until the enumerator is exhausted.

Here’s the code generated for an await foreach:

E e = ((C)(x)).GetAsyncEnumerator();
try
{
    while (await e.MoveNextAsync())
    {
        V v = (V)(T)e.Current;
        // body
    }
}
finally
{
    await e.DisposeAsync();
}

You may notice in the relevant APIs (copied below) that GetAsyncEnumerator accepts a CancellationToken parameter. But await foreach doesn’t make use of this parameter (it passes a default value).

This raises two questions: 1) how do you write an async enumerable with support for cancellation? and 2) how do you consume one?

Writing an async enumerable supporting cancellation

Let’s say that you intend to write IAsyncEnumerable<int> GetItemsAsync(int maxItems) supporting cancellation.

If you just declared the method as async IAsyncEnumerable<int> GetItemsAsync(int maxItems, CancellationToken token), you would be able to pass a cancellation token as an argument, then use it in the body of the method.
But the resulting async-iterator would not properly implement the IAsyncEnumerable.GetAsyncEnumerator(CancellationToken) API, because it would drop the cancellation token passed to it.
The solution is to declare the method as async IAsyncEnumerable<int> GetItemsAsync(int maxItems, [EnumeratorCancellation] CancellationToken token). Because of the attribute, the token parameter will be set to a synthesized cancellation token that combines two token: the one passed as an argument to the method, and the other given to GetAsyncEnumerator. This synthesized token gets cancelled when either of the two given tokens is cancelled.

async IAsyncEnumerable<int> GetItemsAsync(int maxItems, [EnumeratorCancellation] CancellationToken token)
{
    // Your method body using:
    // - `_maxItems`
    // - `token.ThrowIfCancelled();`
    // - `await` and `yield` constructs
}

Consuming an async enumerable with cancellation

There are two scenarios for consuming an async enumerable:

  1. If the method that creates the async enumerable has a cancellation token parameter marked with [EnumeratorCancellation], then just call that method with the cancellation token you need:
    await foreach (var item in GetItemsAsync(maxItems: 10, token)) ...
  2. If the async enumerable instance is given to you, or is constructed in a way to doesn’t capture the desired cancellation token, then you can feed the cancellation token using the WithCancellation<T>(this IAsyncEnumerable<T> source, CancellationToken cancellationToken) extension method:
    await foreach (var item in asyncEnumerable.WithCancellation(token)) ...

The WithCancellation helper method wraps the enumerable from GetItemsAsync along with the given cancellation token. When GetAsyncEnumerator() is invoked on this wrapper, it calls GetAsyncEnumerator(token) on the underlying enumerable.

Appendix: relevant interfaces

using System.Threading;

namespace System.Collections.Generic
{
    /// <summary>Exposes an enumerator that provides asynchronous iteration over values of a specified type.</summary>
    /// <typeparam name="T">The type of values to enumerate.</typeparam>
    public interface IAsyncEnumerable<out T>
    {
        /// <summary>Returns an enumerator that iterates asynchronously through the collection.</summary>
        /// <param name="cancellationToken">A <see cref="CancellationToken"/> that may be used to cancel the asynchronous iteration.</param>
        /// <returns>An enumerator that can be used to iterate asynchronously through the collection.</returns>
        IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
    }

    /// <summary>Supports a simple asynchronous iteration over a generic collection.</summary>
    /// <typeparam name="T">The type of objects to enumerate.</typeparam>
    public interface IAsyncEnumerator<out T> : IAsyncDisposable
    {
        /// <summary>Advances the enumerator asynchronously to the next element of the collection.</summary>
        /// <returns>
        /// A <see cref="ValueTask{Boolean}"/> that will complete with a result of <c>true</c> if the enumerator
        /// was successfully advanced to the next element, or <c>false</c> if the enumerator has passed the end
        /// of the collection.
        /// </returns>
        ValueTask<bool> MoveNextAsync();

        /// <summary>Gets the element in the collection at the current position of the enumerator.</summary>
        T Current { get; }
    }
    
    /// <summary>Provides a mechanism for releasing unmanaged resources asynchronously.</summary>
    public interface IAsyncDisposable
    {
        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or
        /// resetting unmanaged resources asynchronously.
        /// </summary>
        ValueTask DisposeAsync();
    }
}

Original code for IAsyncEnumerable, IAsyncEnumerator and IAsyncDisposable.

For further details, see the async-streams design doc.

Using C# 7.1

C# 7.0 was released as part of Visual Studio 2017 (version 15.0). While we work on C# 8.0, we will also ship features that are ready earlier as point releases.

C# 7.1 is the first such release. It will ship along with Visual Studio 2017 version 15.3. To try it out today, you can install Visual Studio Preview side-by-side, quickly and safely.

As you start using new C# 7.1 features in your code, a lightbulb will offer you to upgrade your project, either to “C# 7.1” or “latest”. If you leave your project’s language version set to “default”, you can only use C# 7.0 features (“default” means the latest major version, so does not include point releases).

Note: make sure you select Configuration All Configuration, as Debug is the configuration selected by default when editting a project.

LangVer7_1.png

Here are more specific instructions for using C# 7.1 in ASP.NET and ASP.NET Core and .NET CLI. The NuGet compiler packages for this release are versioned 2.3.

You can provide feedback on the C# features on the Roslyn repository or via the “Report a Problem” button in Visual Studio.

C# 7.1 features

In addition to numerous issues fixed in this release, the compiler comes with the following features for C# 7.1 (summarized below): async Main, pattern-matching with generics, “default” expressions, and inferred tuple names.

You can find more details about C# 7.1 and our progress on C# 7.2 and 8.0 in the language feature status page.

Async Main

This makes it easier to get started with async code, by recognizing static async Task Main() {...await some asynchronous code...} as a valid entry-point to your program.

Pattern-matching with generics

This allows using open types in type patterns. For example, case T t:.

“default” literal

This lets you omit the type in the default operator (default(T)) when the type can be inferred from the context. For instance, you can invoke void M(ImmutableArray<int> x) with M(default), or specify a default parameter value when declaring void M(CancellationToken x = default).

DefaultError.png

DefaultLightbulb.png

Inferred tuple names

This is a refinement on tuple literals (introduced in 7.0) which makes tuple element names redundant when they can be infered from the expressions. Instead of writing var tuple = (a: this.a, b: X.Y.b), you can simply write var tuple = (this.a, X.Y.b);. The elements tuple.a and tuple.b will still be recognized.

InferredTupleNameLightbulb.png

Error version pragma

This is a small undocumented feature to assist with troubleshooting language version issues. Type #error version and you will see the version of the compiler that you’re using, as well as your current language version setting. ErrorVersion.png

ValueTuple availability on various platforms

As part of VS2017, we have just released C# and VB support for tuples, which Mads describes in the C# 7.0 announcement post.

Under the covers, C#/VB tuples and corresponding F# 4.1 “struct tuples” are lowered into ValueTuple types of various arities and nesting, and tuple element names are stored in TupleElementNamesAttribute. Vlad describes both in some details in “How tuples relate to ValueTuple” and “More about tuple element names”.

Since the early prototyping work for tuples, we not only focused on language questions, but more generally the end-to-end experience of tuples. Central to that experience is how to make the ValueTuple types available.

Without those types, the compilation will fail and reports error CS8179: Predefined type 'System.ValueTuple`2' is not defined or imported.

In order to maximize scenarios were you can use tuples, we took a two-pronged approach:

  1. provide System.ValueTuple nuget package with support with existing frameworks,
  2. migrate ValueTuple and other types into core libraries as updated frameworks ship.

Naturally, a common question is: how soon can I use tuples without referencing this additional ValueTuple package?

Here is the latest status on migrating ValueTuple types into frameworks (as of April 2017) and the planned shipping vehicles (to the best of my knowledge):

  Version that includes ValueTuple
Full/desktop framework .NET Framework 4.7 and Windows 10 Creators Edition Update (RS2)
Core 2.0 (with planned preview in Q2 2017, release in Q3, see roadmap)
Mono Mono 5.0
.Net Standard .Net Standard 2.0

For older frameworks, the ValueTuple package should help fill the gap, including targets for netstandard1.0 and portable-net40+sl4+win8+wp8 (for Portable Class Libraries). I will keep the package updated as the migration into core libraries progresses, to provide as smooth and transparent an experience as possible.

This picture is further complicated as the ValueTuple types receive some minor updates (such as binary serializability). Such improvements will not be available to users of the ValueTuple package; they will only be implemented in frameworks themselves.

I keep track of all the library work related to ValueTuple types in this work items list. But if you have a specific question, it is probably easier to ask me directly in the comments section. Bug reports should go in the corefx github repo.

Known issues:

  • Degraded QuickWatch experience when debugging an application compiled on the full framework 4.6 or earlier on a machine with 4.7 installed. See issue details. This will be mitigated in first quarterly release of VS2017 and expected to be fully fixed in 4.7.1.