TPL与响应式框架的比较

50

在什么情况下会选择使用 Rx 而不是 TPL?还是这两个框架互不干扰?

据我所知,Rx 主要旨在提供对事件的抽象,并允许组合,但它也允许提供异步操作的抽象。可以使用 Createxx 重载和 Fromxxx 重载以及通过释放返回的 IDisposable 进行取消。

TPL 也通过 Task 提供了一个操作抽象和取消的能力。

我的问题是何时使用哪个框架以及用于什么场景?


旧的、关闭的、重复的问题链接:https://dev59.com/63I95IYBdhLWcg3w1BdN#2188259 - yzorg
5个回答

50

Rx的主要目的并不是为事件提供抽象,这只是它的一个结果。它的主要目的是为集合提供可组合的推送模型。

响应式框架(Rx)基于IObservable<T>作为IEnumerable<T>的数学对偶。因此,我们可以通过IObservable<T>接收对象的“推送”,而不是使用IEnumerable<T>从集合中“拉”项。

当然,当我们实际寻找可观测源时,事件和异步操作是很好的选择。

响应式框架自然需要一个多线程模型,以便能够监视可观测数据源并管理查询和订阅。Rx实际上大量使用TPL来实现这一点。

因此,如果使用Rx,则会隐含地使用TPL。

如果您希望直接控制您的任务,则可以直接使用TPL。

但如果您有数据源要观察,并执行查询,请强烈推荐使用响应式框架。


2
我认为,如果您忽略线程问题,短语“提供事件的抽象”等同于“对事件对象集合(也称DTO对象)使用推模型”。 - yzorg
4
非常好的答案,在实际应用中,大部分都是正确的。但是 Rx 可以完全不使用 TPL。此外,Rx 可以在完全单线程的应用程序中使用,完全没有任何并发性(就像 WPF 或 NodeJS 如何在单线程中实现异步一样)。我真的很喜欢你的电梯演讲,除了最后一个词,我会说“它的主要目的是为数据序列提供可组合的推模型。”我发现开发人员越早考虑序列而不是集合,他们就越快理解 Rx。 - Lee Campbell

28

以下是我喜欢遵循的一些准则:

  • 如果处理的是不由自己产生的数据,即随时会到来的数据,则使用RX。
  • 如果是自己生成计算并需要处理并发,则使用TPL。
  • 如果需要管理多个结果,并根据时间进行选择,则使用RX。

15

更新,2016年12月:如果你有30分钟的时间,我建议你阅读乔·达菲(Joe Duffy)的第一手经历,而不是我的猜测。我认为我的分析还是很不错的,但如果你发现了这个问题,我强烈建议你查看博客文章,因为除了TPL vs Rx.NET之外,他还涵盖了MS研究项目(Midori,Cosmos)。

http://joeduffyblog.com/2016/11/30/15-years-of-concurrency/


我认为微软在.NET 2.0发布后进行了过度修正,犯了一个大错误。他们同时引入了许多不同的并发管理API,这些API来自公司的不同部门。

  • Steven Toub强烈推动使用线程安全原语来替换Event(起初是Future<T>,变成了Task<T>
  • 微软研究院有MIN-LINQ和Reactive Extensions (Rx)
  • 硬件/嵌入式系统拥有机器人运行时 (CCR)
同时,许多托管 API 团队试图使用 APM 和 Threadpool.QueueUserWorkItem() 来生存,不知道 Toub 是否会赢得在 mscorlib.dll 中发布 Future<T>/Task<T> 的战斗。最终看起来他们进行了对冲,并在 mscorlib 中同时发布了 Task<T>IObservable<T>,但没有允许任何其他 Rx API(甚至是 ISubject<T>)在 mscorlib 中。我认为这种对冲最终导致了公司内外大量的重复(稍后会有更多内容)和浪费的努力。
关于重复,请参见:Task vs. IObservable<Unit>Task<T> vs. AsyncSubject<T>Task.Run() vs. Observable.Start()。而这只是冰山一角。但在更高的层面上考虑:
  • StreamInsight - SQL事件流,本地代码优化,但事件查询使用LINQ语法定义
  • TPL Dataflow - 建立在TPL之上,与Rx并行构建,优化了调整线程并行性,不擅长组合查询
  • Rx - 令人惊叹的表现力,但充满危险。将“热”流与IEnumerable风格的扩展方法混合使用,这意味着您很容易永久阻塞(在热流上调用First()永远不会返回)。调度限制(限制并行性)通过相当奇怪的SubscribeOn()扩展方法完成,这些方法是难以理解且难以正确使用的。如果开始学习Rx,请预留足够的时间来学习避免所有陷阱。但是,如果需要组合复杂的事件流或需要复杂的过滤/查询,则Rx确实是唯一的选择。

我认为在微软发布mscorlib中的ISubject<T>之前,Rx没有在广泛应用方面有机会。这很令人遗憾,因为Rx包含一些非常有用的具体(通用)类型,例如TimeInterval<T>Timestamped<T>,我认为它们应该像Nullable<T>一样在Core/mscorlib中。此外还有System.Reactive.EventPattern<TEventArgs>


1
滑稽的拼写错误。 - Dave Sexton
1
@DaveSexton IObservable? - yzorg
1
我认为@DaveSexton的意思是SteamInsight。但说真的,感谢您对框架进行最有洞察力的分析!在我看来,这应该是被接受的答案! - kkm
@kkm 我的回答是在问题发布后3年,但还是谢谢你! - yzorg
1
几年后,乔·达菲离开了微软,并对他在那里的职业生涯进行了非常有趣的总结。这也恰好是我见过的最好的回答:http://joeduffyblog.com/2016/11/30/15-years-of-concurrency/ - yzorg

13

我喜欢Scott W的项目符号。为了提供更多具体的例子,Rx非常适合:

  • 消费流
  • 执行像Web请求这样的非阻塞异步工作。
  • 流式事件(.net事件,如鼠标移动或Service Bus消息类型事件)
  • 组合“流”事件
  • Linq风格操作
  • 从公共API公开数据流

TPL似乎很好地映射到:

  • 内部工作的并行化
  • 执行像Web请求这样的非阻塞异步工作
  • 执行工作流和延续

我注意到IObservable(Rx)的一件事是它变得无处不在。一旦进入您的代码库,因为它无疑将通过其他接口公开,它最终会出现在您的应用程序中。我想这可能一开始很可怕,但大部分团队现在对Rx感到非常舒适,并且喜欢它为我们节省的工作量。

在我看来,Rx将成为优于TPL的主要库,因为它已经支持.NET 3.5、4.0、Silverlight 3、Silverlight 4和Javascript。这意味着您有效地只需学习一种风格,并且它适用于许多平台。

编辑:我改变了关于Rx优于TPL的看法。它们解决不同的问题,所以不应该像那样进行比较。使用.NET 4.5/C# 5.0,async/await关键字将进一步将我们与TPL联系在一起(这是好的)。有关Rx vs事件vs TPL等的深入讨论,请查看我的在线书籍IntroToRx.com第一章


11

我认为TPL Dataflow覆盖了Rx中专门的功能子集。Dataflow用于需要花费可测量时间进行数据处理的情况,而Rx用于事件(例如鼠标位置、错误状态等),其中处理时间可以忽略不计。

例如:您的“subscribe”处理程序是异步的,并且您希望不超过1个执行器在同时运行。使用Rx,您必须进行阻塞,因为Rx是异步无关的,在许多地方不会以特殊方式对待异步。

.Subscribe(myAsyncHandler().Result)

如果您不阻止操作,那么Rx将认为该操作已完成,而处理程序仍在异步执行中。

您可能会认为,如果您执行

.ObserveOn(Scheduler.EventLoopSchedule)

问题得到解决。但这将打破您的.Complete()工作流,因为Rx会认为它已经完成了调度执行,并且您将在等待异步操作完成之前退出应用程序。

如果您想允许不超过4个并发异步任务,则Rx没有提供任何开箱即用的东西。也许您可以通过实现自己的调度程序、缓冲区等来进行一些黑客技巧。

TPL Dataflow在ActionBlock中提供了非常好的解决方案。它可以限制同时执行的操作数,并且确实理解异步操作,因此调用Complete()并等待Completed将会做到您所期望的:等待所有正在进行的异步任务完成。

TPL还有另一个特性是“反压”。假设您发现处理程序中存在错误,并且需要重新计算上个月的数据。如果您使用Rx订阅源,而您的管道包含无界缓冲区或ObserveOn,那么源将保持比处理速度更快的读取速度,几秒钟内就会耗尽内存。即使您实现了阻塞消费者,您的源也可能受到阻塞调用的影响,例如如果源是异步的。在TPL中,您可以将源实现为

while(...)
    await actionBlock.SendAsync(msg)

它不会阻塞源,但在处理程序超载时将等待。

总的来说,我发现 Rx 很适合那些时间和计算量较小的操作。如果处理时间变得相当长,则会涉及到奇怪的副作用和深奥的调试。

好消息是 TPL 数据流块非常适用于 Rx。它们有 AsObserver/AsObservable 适配器,并且在需要时可以将其插入 Rx 管道中间。但 Rx 有更多的模式和用例。因此,我的经验法则是从 Rx 开始,根据需要添加 TPL 数据流。


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