使用 EnumeratorCancellation 返回 AsyncEnumerable 或循环 WithCancellation 之间有什么区别

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

我有以下从 http 流中读取 csv 文档的方法

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>();
    }
}

以及其他地方使用结果的方法

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

我的问题是我应该只在一个地方使用取消令牌吗?是否应该在产生记录之前对取消令牌采取行动,或者 IAsyncEnumerable.WithCancellation() 基本上做同样的事情?如果有区别是什么?

根据 sources

,无论如何,取消令牌都会传递给 GetAsyncEnumerator 方法
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; }
    }
}

你应该只使用一次cancellationToken,直接传递或者使用WithCancellation,这些方法都是一样的。 WithCancellationIAsyncEnumerable<T> 的扩展方法,接受 CancellationToken 作为参数(它使用与 ConfigureAwait 相同的模式)。在 [EnumeratorCancellation] 的情况下,编译器生成将令牌传递给 GetAsyncEnumerator 方法

的代码

两种不同方式的原因详见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?

取消是合作的,因此为了能够取消,您必须中实施取消生产者 代码 GetLines,提供 IAsyncEnumerable<Line> 的代码。所以,生产者是一个地方。

现在,假设使用该数据执行某些操作的代码的方法被命名为 ConsumeLines,假设它是一个 消费者 。在您的情况下,它可能是一个代码库,但一般来说,它可能是另一个库、另一个存储库、另一个代码库。

在其他代码库中,无法保证它们具有 相同 CancellationToken.

那么,消费者如何取消?

消费者需要将 CancellationToken 传递给 IAsyncEnumerable<T>.GetAsyncEnumerator,但如果您使用 await foreach 构造,它不会直接公开。

为了解决这个问题,添加了 WithCancellation 扩展方法。它只是将传递给它的 CancellationToken 转发给底层 IAsyncEnumerable,方法是将其包装在 ConfiguredCancelableAsyncEnumerable.

根据几个条件,这个 CancellationToken 使用 CreateLinkedTokenSource 被 link 编辑到生产者中的那个,这样 消费者可以使用在生产者所以我们不仅可以取消消费,还可以取消生产.

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?

,您应该在 生产者 代码中使用 IsCancellationRequested or ThrowIfCancellationRequested 来对您的 CancellationToken 采取行动。取消是合作的,如果你不在producer中实现它,你将无法取消producing [=19=的值].

至于何时取消——在屈服之前或之后——完全取决于你,我们的想法是避免任何不必要的工作。本着这种精神,您还可以在方法的第一行检查取消,以避免发送不必要的 http 请求。

请记住,取消值的消耗不一定与取消值的产生相同。

同样,生产者和消费者可能在不同的代码库中,并且可能使用来自不同 CancellationTokenSources.

CancellationTokens

到link那些不同的CancellationTokens你需要一起使用EnumeratorCancellation属性.

请在我的文章中阅读更深入的解释EnumeratorCancellation: CancellationToken parameter from the generated IAsyncEnumerable.GetAsyncEnumerator will be unconsumed