13

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

4

2 回答 2

16

根据消息来源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,这些方法都是一样的。WithCancellation是 的扩展方法IAsyncEnumerable<T>,接受 aCancellationToken作为参数(它使用与 相同的模式ConfigureAwait)。如果[EnumeratorCancellation]编译器生成将令牌传递给GetAsyncEnumerator方法的代码

MSDN杂志中描述了两种不同方式的原因

为什么有两种不同的方法来做到这一点?将令牌直接传递给方法更容易,但是当您从其他来源获得任意 IAsyncEnumerable 但仍希望能够请求取消构成它的所有内容时,它就不起作用了。在极端情况下,将令牌传递给 GetAsyncEnumerator 也是有利的,因为这样做可以避免在多次枚举单个可枚举的情况下“烧毁”令牌:通过将其传递给 GetAsyncEnumerator,可以使用不同的令牌每次都能通过。

于 2019-12-12T08:54:18.827 回答
1

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

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

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

在其他代码库中,不能保证它们具有相同 CancellationToken的.

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

消费者需要将 a 传递CancellationTokenIAsyncEnumerable<T>.GetAsyncEnumerator,但如果您使用await foreach构造,它不会直接暴露。

为了解决这个问题,WithCancellation添加了扩展方法。它只是通过将传递给它的内容包装在ConfiguredCancelableAsyncEnumerableCancellationToken中来将其转发给底层。IAsyncEnumerable

根据几个条件,这CancellationToken与生产者中使用CreateLinkedTokenSource的条件相关联,因此消费者可以使用生产者中实现的合作取消来取消,这样我们不仅可以取消消费,还可以取消生产

是否应该在产生记录之前对取消令牌采取行动,还是 IAsyncEnumerable.WithCancellation() 基本上做同样的事情?如果有,有什么区别?

的,您应该通过在您的生产者代码中CancellationToken使用IsCancellationRequestedThrowIfCancellationRequested来对您采取行动。取消是合作的,如果你不在producer中实现它,你将无法取消产生的值。IAsyncEnumerable

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

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

同样,生产者和消费者可能位于不同的代码库中,并且可能使用CancellationTokens不同的CancellationTokenSources.

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

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

于 2020-07-23T09:18:16.847 回答