3

C# 8 adds support for asynchronuous iterator blocks, so you can await things and return an IAsyncEnumarator instead of an IEnumerable:

public async IAsyncEnumerable<int> EnumerateAsync() {
    for (int i = 0; i < 10; i++) {
        yield return i;
        await Task.Delay(1000);
    }
}

With a non-blocking consuming code that looks like this:

await foreach (var item in EnumerateAsync()) {
    Console.WriteLine(item);
}

This will result in my code running for about 10 seconds. However, sometimes I want to break out of the await foreach before all elements are consumed. With an breakhowever, we would need to wait until the current awaited Task.Delay has finished. How can we break immediately out of that loop without waiting for any dangling async tasks?

Bruno Zell
  • 7,761
  • 5
  • 38
  • 46

1 Answers1

7

The use of a CancellationToken is the solution since that is the only thing that can cancel the Task.Delay in your code. The way we get it inside your IAsyncEnumerable is to pass it as a parameter when creating it, so let's do that:

public async IAsyncEnumerable<int> EnumerateAsync(CancellationToken cancellationToken = default) {
    for (int i = 0; i < 10; i++) {
        yield return i;
        await Task.Delay(1000, cancellationToken);
    }
}

With the consuming side of:

// In this example the cancellation token will be caneled after 2.5 seconds
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2.5));
await foreach (var item in EnumerateAsync(cts.Token)) {
    Console.WriteLine(item);
}

Sure, this will cancel the enumeration after 3 elements were returned, but will end in an TaskCanceledException thrown out of Task.Delay. To gracefully exit the await foreach we have to catch it and break on the producing side:

public async IAsyncEnumerable<int> EnumerateAsync(CancellationToken cancellationToken = default) {
    for (int i = 0; i < 10; i++) {
        yield return i;
        try {
            await Task.Delay(1000, cancellationToken);
        } catch (TaskCanceledException) {
            yield break;
        }
    }
}

Note

As of now this is still in preview and is subject to possible change. If you are interested in this topic you can watch a discussion of the C# language team about CancellationToken in an IAsyncEnumeration.

Bruno Zell
  • 7,761
  • 5
  • 38
  • 46
  • 5
    I just want to add that preview2 of VS2019 and Core 3 will have some better support for `CancellationToken`. We will provide a `.WithCancellationToken()` extension method. See LDM meeting notes: https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-11-28.md#cancellation-of-async-streams – Julien Couvreur Dec 23 '18 at 22:04
  • The Visual Studio 2019 issues a warning if you don't decorate the `CancellationToken` argument with a [`EnumeratorCancellation`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.enumeratorcancellationattribute) attribute. Also suppressing the `TaskCanceledException` in the producer side seems like a non-standard behavior. I think that this exception is conveying the information about whether a cancellation occurred or not, so it should be propagated to the caller. – Theodor Zoulias Apr 04 '20 at 18:31