ChannelReader<T> 和 IAsyncEnumerable<T> 之间的区别

Cow*_*org 6 c# asp.net-core system.threading.channels

我会保持这个简短

阅读“在 ASP.NET Core SignalR 中使用流式传输”一文(第一部分,服务器到客户端流式传输)后, ChannelReader[T]IAsyncEnumerable[T]之间有什么区别?

Dai*_*Dai 1

  • 好吧,细微的区别是:

    • ChannelReader<T>是一个abstract class
    • IAsyncEnumerable<T>是一个协变 interface
    • 虽然任何人都可以子类化ChannelReader<T>- 就像任何人都可以实现一样IAsyncEnumerable<T>-实际上,您只会使用从 . 返回internal的具体子类。ChannelReader<T>Channel<T>.Reader
  • 虽然ChannelReader<T>没有实现IAsyncEnumerable<T>,但它确实通过其方法公开了IAsyncEnumerable<T> 自身的视图ReadAllAsync

  • 笔者(合理)认为两者之间的默示合同存在差异:

    • 对于 an IEnumerable<T>(包括IAsyncEnumerable<T>),人们普遍期望IEnumerable<T>具有有限的大小(即,最终foreach( var x in someEnumerable ) {} 完成而不是永远循环)...
    • ...而 a Channel<T>(这是 a 的来源ChannelReader<T>可能永远不会完成。
    • ...这意味着如果永远不会完成,做这样的事情(如下)将会导致问题ChannelReader<T>
      List<T> list = new List<T>();
      await foreach( var x in myChannelReader.ReadAllAsync() ) list.Add( x );
      
      Run Code Online (Sandbox Code Playgroud)
    • 我注意到,在 ASP.NET Core 文档示例的链接页面中ChannelReader<T>(最终)调用了 该ChannelWriter<T>.Complete()方法,这确保了事情不会失控。
      • 这在 SignalR 中将是一个问题,因为如果您仅返回一个ChannelReader<T>对象,则该对象的接收者无法ChannelReader<T>到达底层Channel<T>或其姐妹ChannelWriter<T>对象,因此该ChannelWriter<T>.Complete()方法将永远不会被调用
        • ...这是一个问题。
        • ...文档中提到了这个问题,因此文档建议您改为使用IAsyncEnumerable<T> ,因为它可以由其使用者取消/中止,而ChannelReader<T> 不能

          异步迭代器方法避免了 Channel 常见的问题,例如没有足够早地返回 ChannelReader 或在没有完成ChannelWriter<T> [..] 完成的情况下退出方法Channelfinally

        • ...因此,如果您想安全地返回一个永无止境的项目序列(不使用 aChannel<T>作为源),那么请考虑通过将其表示为 的自定义实现来返回该序列IAsyncEnumerable<T>

以方便的表格形式:

ChannelReader<T> IAsyncEnumerable<T>
通常暗示(在 SignalR 之外)具有有限的大小 是的
可以无限大小(即可以永远迭代 是的 是的
可以由 SignalR 安全地完成(即停止) 是的

其他注意事项:

  • 最初我认为返回 aChannelReader<T>总是安全的,因为我希望在收到信号时ChannelReader<T>.ReadAllAsync(CancellationToken)调用,但据我所知,令牌只会取消当前的读取操作,而不会完成本身。ChannelWriter<T>.Complete()CancellationTokenChannel<T>
  • SignalR确实支持 CompletingChannel<T>对象,但仅限于它自己创建的对象(在 内部internal class StreamTracker),而不是当您返回ChannelReader<T>.