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

6
1个回答

1
嗯,微小的差异在于: - `ChannelReader` 是一个抽象类。 - `IAsyncEnumerable` 是一个协变接口。 - 虽然任何人都可以子类化 `ChannelReader` - 就像任何人都可以实现 `IAsyncEnumerable` - 但在实践中,你只会使用从 `Channel.Reader` 返回的 `ChannelReader` 的内部具体子类。 - 虽然 `ChannelReader` 不实现 `IAsyncEnumerable`,但它通过其 `ReadAllAsync` 方法公开了一个对自身的 `IAsyncEnumerable` 视图。
本作者认为这两者之间存在着隐含的契约差异: - 对于 `IEnumerable`(包括 `IAsyncEnumerable`),人们普遍期望 `IEnumerable` 具有有限的大小(即执行 `foreach( var x in someEnumerable ) {}` 最终会完成,而不是无限循环)... - ...而一个 `Channel`(作为 `ChannelReader` 的源)可能永远不会完成。 - 这意味着如果 `ChannelReader` 永远不会完成,像下面这样的操作将会导致问题: ``` List list = new List(); await foreach( var x in myChannelReader.ReadAllAsync() ) list.Add( x ); ``` - 我注意到在 ASP.NET Core 文档的链接页面中,使用 `ChannelReader` 的示例会调用 `ChannelWriter.Complete()` 方法(最终),以确保事情不会失控。 - 这在 SignalR 中是个问题,因为如果你只返回一个 `ChannelReader` 对象,接收该 `ChannelReader` 的接收方无法访问底层的 `Channel` 或其姐妹 `ChannelWriter` 对象,因此 `ChannelWriter.Complete()` 方法将永远不会被调用。 - 这是个问题。 - 文档中提到了这个问题,并建议你使用 `IAsyncEnumerable` 来代替,因为它可以被其消费者取消/中止,而 `ChannelReader` 不能。 - 因此,如果你想要安全地返回一个永不结束的项目序列(而不使用 `Channel` 作为源),那么考虑将该序列表示为 `IAsyncEnumerable` 的自定义实现。
以便于查看的表格形式:
ChannelReader<T> IAsyncEnumerable<T>
通常(在SignalR之外)被认为是有限大小的
可以潜在地具有无限大小(即可以无限迭代)
可以被SignalR安全地完成(即停止)

杂项说明:
- 最初我认为返回一个 `ChannelReader` 总是安全的,因为我期望 `ChannelReader.ReadAllAsync(CancellationToken)` 在 `CancellationToken` 被触发时会调用 `ChannelWriter.Complete()`,但据我所知,令牌只会取消当前的读取操作,而不会完成 `Channel` 本身。
- SignalR 确实支持完成 `Channel` 对象,但只限于它自己创建的对象(在 `internal class StreamTracker` 中),而不是当你返回一个 `ChannelReader` 时。

对于“IAsyncEnumerable<T>具有有限大小的一般期望”我不太确定。从ChannelReader<T>获取IAsyncEnumerable<T>并不罕见,所以对于一个的期望也应该适用于另一个。 - undefined
1
@TheodorZoulias 对 - 我并不是说这是“不可能的”(事实上,恰恰相反)- 我应该把解释表述为我的观点,而不是硬性事实或者.NET规范中的内容,甚至是指南 - 所以我只是基于我的经验和已建立的惯例。 - undefined
你还可以注意到,IAsyncEnumerable<T>具有的机制(而ChannelReader<T>缺少的)用于通知其完成的方法是DisposeAsync方法,该方法属于IAsyncEnumerator<T>。当await foreach循环正常或异常退出时,DisposeAsync会自动调用。 - undefined
@TheodorZoulias 我考虑过这个问题,是的 - 但问题是,由 ChannelReader<T>.ReadAllAsync 实现的 (编译器生成的) IAsyncEnumerable<T> 的返回类型在调用和等待其 DisposeAsync 方法时不会 Complete() 写入器。真是气人。 - undefined
是的,这是因为ChannelReader<T>可以被多个工作线程消费。如果一个通道因为其中一个消费者由于异常意外死亡而自动Complete,那就没有意义了。ChannelReader<T>.ReadAllAsyncDisposeAsync信号没有用处,所以它会忽略它(迭代器中没有finally)。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接