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?
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With