任务、TPL数据流和异步/等待,什么时候使用哪个?

12

我阅读过很多技术文档,包括一些由Microsoft团队或其他作者编写的有关新TPL Dataflow库、async/await并发框架和TPL功能的详细说明。然而,我并没有真正找到清晰地区分它们各自应该在何时使用的内容。我知道每个工具都有其适用性和应用场景,但具体来说:

我有一个完全在进程内运行的数据流模型。在顶部是一个数据生成组件(A),它生成数据并通过数据流块链接或通过触发事件传递给处理组件(B)。其中的一些部分必须同步运行,而(A)大量受益于并行性,因为大多数过程都是I/O或CPU绑定的(从磁盘读取二进制数据,然后对其进行反序列化和排序)。最终,处理组件(B)将转换后的结果传递给(C)以供进一步使用。

具体来说,我想知道何时使用任务(tasks)、async/await和TPL数据流块以解决以下问题:

  • 启动数据生成组件(A)。我显然不想锁定gui/dashboard界面,因此这个过程必须在不同的线程/任务上运行。

  • 如何调用(A)、(B)和(C)中未直接涉及数据生成和处理过程的方法,但执行配置工作可能需要几百毫秒/秒来返回。我的直觉是这正是async/await发挥作用的地方?

  • 我最困扰的是如何最好地设计组件之间的消息传递。TPL Dataflow看起来非常有趣,但有时对我的目的来说太慢了。如果不使用TPL Dataflow,如何通过进程内的任务并发传递数据以实现响应性和并发性?例如,显然,如果在任务中引发事件,则订阅的事件处理程序将在同一任务中运行,而不是传递到另一个任务中。简而言之,在将数据传递给组件(B)后,组件(A)如何继续其业务,同时组件(B)检索数据并专注于处理它?哪种并发模型在这里最好使用?

我在这里实现了数据流块,但这真的是最好的方法吗?

  • 我想总结上述问题,我的困境就是如何使用标准实践设计和实现API类型组件?方法是否应该设计为异步,数据输入作为数据流块,数据输出作为数据流块或事件?通常哪种方法是最好的?我问这个问题是因为上面提到的大多数组件都应该独立工作,所以它们可以基本上被交换或内部独立地修改而不必重写访问器和输出。

  • 关于性能的注意事项:我提到TPL Dataflow块有时很慢。我处理高吞吐量、低延迟类型的应用程序,目标是磁盘I/O极限,因此TPL Dataflow块的性能通常比例如同步处理单元要慢得多。问题在于我不知道如何将进程嵌入到自己的任务或并发模型中,以实现与TPL Dataflow块已经处理的类似功能,但没有TPL df带来的开销。


    “一般来说,最好的方法是什么?” 我认为并没有一个最好的方法。编程通常涉及考虑各种替代方案,并且在所有情况下都没有一个明显的最佳方案。这也是为什么当您关心性能时,应该使用分析工具,因为没有“X总是比Y更好”。 - svick
    7
    @svick,我非常尊重您的专业知识和意见,但这就是为什么我明确指定了一个用例,并在询问哪种方法最适合时提出了具体、有针对性的问题。您能否分享一下您如何处理这四个要点的知识?非常感谢。 - Matt
    请随意在[元]上提出闭包问题。 - casperOne
    @Freddy 不是的,因为你在错误的地方表示不同意。这是第一次(到目前为止有两次)指向[元]。评论是为了澄清问题。它们不是用于元评论(问题是否开放、关闭等)。 - casperOne
    2个回答

    12

    听起来你正在使用“推送”系统。普通的async代码只处理“拉取”场景。

    你的选择在TPL Dataflow和Rx之间。我认为TPL Dataflow更容易学习,但既然你已经尝试过它,而且它对你的情况不起作用,那么我会尝试Rx。

    Rx从非常不同的角度解决了这个问题:它以“事件流”为中心,而不是TPL Dataflow的“演员网络”。最近版本的Rx非常友好,因此您可以在Rx管道的几个点上使用async委托。

    关于您的API设计,TPL Dataflow和Rx都提供了您应该实现的接口:IReceivableSourceBlock/ITargetBlock用于TPL Dataflow,IObservable/IObserver用于Rx。您只需将实现连接到内部mesh(TPL Dataflow)或查询(Rx)的端点即可。这样,您的组件只是一个“块”或“可观察/观察者/主题”,可以组成其他“网格”或“查询”。

    最后,对于您的async构建系统,您只需要使用工厂模式。您的实现可以调用Task.Run在线程池线程上执行配置。


    你何时会选择使用“推送”系统而不是在异步方法返回结果后编写处理数据的方法?我在阅读了您的几篇文章并推荐使用数据流之后,打开了一个问题这里,但我仍然不确定相比于等待异步任务后添加处理方法,它的好处是否完全理解,请详细说明或引用我在那个问题中编写的示例。 - BornToCode
    大多数系统本质上是“推”或“拉”,如果您想以另一种方式使用它们,则需要进行翻译(后台进程/缓冲区)。例如,从数据库检索记录是“拉”系统;用户输入是“推”系统。我更喜欢使用适合当前情况的方法;例如,对于数据库查询,请使用async,对于响应UI输入,请使用事件/Rx。 - Stephen Cleary

    0

    只是想在这里留下一些东西,如果它能帮助某人了解何时使用数据流,因为我对TPL Dataflow的性能感到惊讶。我有一个如下场景:

    • 遍历项目中的所有C#代码文件(约3500个文件)
    • 读取所有文件行(IO操作)
    • 遍历所有文件行并查找其中的一些字符串
    • 返回具有搜索字符串的文件及其行

    我认为这是TPL Dataflow的一个非常好的例子,但当我只为需要打开的每个文件生成了一个新的Task,并在该特定任务中完成了所有逻辑时,这段代码更快。

    由此得出我的结论是,默认情况下使用Await/Async/Task实现,至少对于这种简单的任务,而TPL Dataflow是为更复杂的情况而设计的,特别是当您需要批处理和其他更“推送”式的场景以及同步更成问题时。

    编辑:然后我对此进行了更多研究,并创建了一个演示项目,结果非常有趣。因为随着我们拥有更多的操作并且它们变得更加复杂,TPL Dataflow变得更加高效。

    这里是存储库的链接。


    这不是一个有用的答案,因为它没有包括特定实验的代码。我的猜测是,管道中的任务不是粗粒度的,正如文档的第一段所述。如果你传递了大量的小字符串,并且每个字符串只进行了微不足道的计算,那么开销将会很大。这可以通过将工作负载分块来轻松解决,例如使用Chunk LINQ操作符或BatchBlock<T> - Theodor Zoulias
    @TheodorZoulias 我想我没有做错什么:),但如果您感兴趣,现在可以检查演示项目并自行查看。感谢您的提醒:),初始回复不够可靠。 - DomenPigeon
    这里的问题和答案应该是自主的。外部链接随着时间的推移会变成死链接,包含它们的问题/答案将失去大部分价值。您能否在答案中包含一个最小的TPL Dataflow示例,以重现您的观察结果?您应该能够构思一个简单的示例,最多只有40-50行代码,展示您实验的本质,并且可以通过社区成员的最小努力进行审查。 - Theodor Zoulias

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