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

时间:2019-12-12 08:38:39

标签: c# .net-core c#-8.0

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

2 个答案:

答案 0 :(得分:3)

根据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

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

为什么有两种不同的方法呢?将令牌直接传递给   方法更简单,但是当您随意处理   IAsyncEnumerable来自其他来源,但仍然希望能够   要求取消构成它的所有内容。在   在极端情况下,将令牌传递给   GetAsyncEnumerator这样做可以避免“烧毁”令牌中的令牌。   单个枚举将被多次枚举的情况:   将其传递给GetAsyncEnumerator,每个传递一个不同的令牌   时间。

答案 1 :(得分:1)

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

取消是合作的,因此为了能够取消,您必须生产者代码{{1}中实施取消},提供GetLines。因此,生产者是一个地方。

现在,假设将处理该数据的代码命名为IAsyncEnumerable<Line>的方法,假设它是一个消费者。在您的情况下,它可能是一个代码库,但通常来说,它可能是另一个库,另一个存储库,另一个代码库。

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

那么,消费者如何取消?

消费者需要将CancellationToken传递给CancellationToken,但是如果您使用的是IAsyncEnumerable<T>.GetAsyncEnumerator结构,则不会直接暴露它。

为解决此问题,添加了await foreach扩展方法。只需将传递给它的WithCancellation包装在ConfiguredCancelableAsyncEnumerable中,就可以将传递给它的CancellationToken转发到基础IAsyncEnumerable

根据几个条件,此CancellationToken使用CreateLinkedTokenSource链接到生产者中的一个,以便消费者可以使用生产者中实施的协作取消来取消,这样我们不仅可以取消消费,也可以制作

应该在产生记录之前对取消令牌进行操作,还是IAsyncEnumerable.WithCancellation()基本上在做相同的事情?有什么区别?

,您应该通过在生产者代码中使用IsCancellationRequestedThrowIfCancellationRequestedCancellationToken进行操作。取消是协作的,如果不在 producer 中实现,则将无法取消 production IAsyncEnumerable值。

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

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

同样,生产者和消费者可能使用不同的代码库,并且可能使用来自不同CancellationTokens的{​​{1}}。

要链接那些不同的 CancellationTokenSources ,您需要一起使用 CancellationTokens 属性

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

中的更深入的解释