返回带有枚举器取消的异步枚举或循环使用取消有什么区别



我有以下方法可以从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.WithCancel(( 基本上在做同样的事情?如果有的话有什么区别?

根据消息来源,在引擎盖下,取消令牌无论如何都会传递给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杂志中描述了两种不同方式的原因

为什么有两种不同的方法?将令牌直接传递给 方法更容易,但当你被递给任意时它不起作用 IAsyncEnumerable从其他来源,但仍希望能够 要求取消组成它的所有内容。在 在极端情况下,将令牌传递给 GetAsyncEnumerator,因为这样做可以避免"刻录"令牌 单个可枚举对象将被多次枚举的情况:通过 将其传递给 GetAsyncEnumerator,每个都可以传递不同的令牌 时间。

我的问题是我应该只在一个地方使用取消令牌吗?

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

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

在另一个代码库中,不能保证它们具有相同的CancellationToken

那么,消费者如何取消呢?

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

为了解决这个问题,添加了WithCancellation扩展方法。它只是通过将传递给它的CancellationToken转发到基础IAsyncEnumerable,方法是将其包装在 ConfigurationCancelableAsyncEnumerable 中。

根据几个条件,此CancellationToken使用 CreateLinkedTokenSource 链接到生产者中的,以便消费者可以使用生产者中实现的合作取消取消,因此我们不仅可以取消消费,还可以取消生产

应该在生成记录之前对取消令牌进行操作,还是 IAsyncEnumerable.WithCancel(( 基本上在做同样的事情?如果有的话有什么区别?

是的,您应该通过在生产者代码中使用 IsCancelRequest 或 ThrowIfCancelRequest 来对CancellationToken进行操作。取消是合作的,如果不在生产者中实现它,您将无法取消生成IAsyncEnumerable的值。

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

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

同样,生产者和消费者可能位于不同的代码库中,并且可以使用来自不同CancellationTokenSourcesCancellationTokens

若要将这些不同的CancellationTokens链接在一起,需要使用EnumeratorCancellation属性

请阅读我的文章 EnumeratorCancel: CancelToken 参数中生成的 IAsyncEnumerable.GetAsyncEnumerator 将未使用

最新更新