Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the difference between returning AsyncEnumerable with EnumeratorCancellation or looping WithCancellation

I have the following method that reads a csv document from a http stream

public async IAsyncEnumerable<Line> GetLines([EnumeratorCancellation] CancellationToken cancellationToken)
{
    HttpResponseMessage response = GetResponse();

    using var responseStream = await response.Content.ReadAsStreamAsync();
    using var streamReader = new StreamReader(responseStream);
    using var csvReader = new CsvReader(streamReader);

    while (!cancellationToken.IsCancellationRequested && await csvReader.ReadAsync())
    {
        yield return csvReader.GetRecord<Line>();
    }
}

and a method elsewhere that uses the result

var documentAsyncEnumerable = graphClient.GetLines(cancellationToken);
await foreach (var document in documentAsyncEnumerable.WithCancellation(cancellationToken))
{
    // Do something with document    
}

My question is shouldn I use the cancellation token in just one place? Should the cancellation token be acted upon before yielding the record or is the IAsyncEnumerable.WithCancellation() basically doing the same thing? What is the difference if any?

like image 511
Homulvas Avatar asked Dec 12 '19 08:12

Homulvas


2 Answers

Under the hood the cancellation token is passed to GetAsyncEnumerator method anyway, according to the sources

namespace System.Collections.Generic
{
    public interface IAsyncEnumerable<out T>
    {
        IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
    }

    public interface IAsyncEnumerator<out T> : IAsyncDisposable
    {
        ValueTask<bool> MoveNextAsync();
        T Current { get; }
    }
}

You should use cancellationToken only once, passing directly or use WithCancellation, these methods are doing the same. WithCancellation is extension method for IAsyncEnumerable<T>, accepting a CancellationToken as an argument (it uses the same pattern with ConfigureAwait). In case of [EnumeratorCancellation] the compiler generate code that will pass the token to GetAsyncEnumerator method

The reason of two different ways are described in MSDN magazine

Why two different ways to do it? Passing the token directly to the method is easier, but it doesn’t work when you’re handed an arbitrary IAsyncEnumerable from some other source but still want to be able to request cancellation of everything that composes it. In corner-cases, it can also be advantageous to pass the token to GetAsyncEnumerator, as doing so avoids “burning in” the token in the case where the single enumerable will be enumerated multiple times: By passing it to GetAsyncEnumerator, a different token can be passed each time.

like image 147
Pavel Anikhouski Avatar answered Oct 15 '22 07:10

Pavel Anikhouski


My question is shouldn I use the cancellation token in just one place?

Cancellation is cooperative, so in order to be able to cancel, you had to implement cancellation in the producer code GetLines, the one that provides the IAsyncEnumerable<Line>. So, producer is one place.

Now, imagine that the method that the code that does something with that data is named ConsumeLines, let's say it's a consumer. In your case it could be one codebase, but generally speaking, it could be another library, another repo, another codebase.

In that other codebase, there is no guarantee they have the same CancellationToken.

So, how can a consumer cancel?

Consumer needs to pass a CancellationToken to the IAsyncEnumerable<T>.GetAsyncEnumerator, but it's not directly exposed if you're using await foreach construct.

To solve this, WithCancellation extension method was added. It simply forwards the CancellationToken passed to it to the underlying IAsyncEnumerable by wrapping it in a ConfiguredCancelableAsyncEnumerable.

Depending on several conditions, this CancellationToken is linked to the one in the producer using CreateLinkedTokenSource so that consumer can cancel using cooperative cancellation implemented in the producer so not only we can cancel consuming, but also, producing.

Should the cancellation token be acted upon before yielding the record or is the IAsyncEnumerable.WithCancellation() basically doing the same thing? What is the difference if any?

Yes, you should act upon your CancellationToken by using either IsCancellationRequested or ThrowIfCancellationRequested in your producer code. Cancellation is cooperative, if you don't implement it in producer, you won't be able to cancel producing the values of IAsyncEnumerable.

As to when exactly to cancel - before or after yielding - it totally up to you, the idea is to avoid any unnecessary work. In this spirit, you could also check for cancellation in the first line of your method, to avoid sending an unnecessary http request.

Remember that cancelling the consuming of the values, is not necessarily the same thing as cancelling the producing of the values.

Again, producer and consumer could be in different codebases, and could be using CancellationTokens from different CancellationTokenSources.

To link those different CancellationTokens together you need to use the EnumeratorCancellation attribute.

Please read a more in-depth explanation in my article EnumeratorCancellation: CancellationToken parameter from the generated IAsyncEnumerable.GetAsyncEnumerator will be unconsumed

like image 33
ironstone13 Avatar answered Oct 15 '22 07:10

ironstone13