在什么情况下会选择使用 Rx 而不是 TPL?还是这两个框架互不干扰?
据我所知,Rx 主要旨在提供对事件的抽象,并允许组合,但它也允许提供异步操作的抽象。可以使用 Createxx 重载和 Fromxxx 重载以及通过释放返回的 IDisposable 进行取消。
TPL 也通过 Task 提供了一个操作抽象和取消的能力。
我的问题是何时使用哪个框架以及用于什么场景?
在什么情况下会选择使用 Rx 而不是 TPL?还是这两个框架互不干扰?
据我所知,Rx 主要旨在提供对事件的抽象,并允许组合,但它也允许提供异步操作的抽象。可以使用 Createxx 重载和 Fromxxx 重载以及通过释放返回的 IDisposable 进行取消。
TPL 也通过 Task 提供了一个操作抽象和取消的能力。
我的问题是何时使用哪个框架以及用于什么场景?
Rx的主要目的并不是为事件提供抽象,这只是它的一个结果。它的主要目的是为集合提供可组合的推送模型。
响应式框架(Rx)基于IObservable<T>
作为IEnumerable<T>
的数学对偶。因此,我们可以通过IObservable<T>
接收对象的“推送”,而不是使用IEnumerable<T>
从集合中“拉”项。
当然,当我们实际寻找可观测源时,事件和异步操作是很好的选择。
响应式框架自然需要一个多线程模型,以便能够监视可观测数据源并管理查询和订阅。Rx实际上大量使用TPL来实现这一点。
因此,如果使用Rx,则会隐含地使用TPL。
如果您希望直接控制您的任务,则可以直接使用TPL。
但如果您有数据源要观察,并执行查询,请强烈推荐使用响应式框架。
以下是我喜欢遵循的一些准则:
更新,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来自公司的不同部门。
Future<T>
,变成了Task<T>
)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()
。而这只是冰山一角。但在更高的层面上考虑:
IEnumerable
风格的扩展方法混合使用,这意味着您很容易永久阻塞(在热流上调用First()
永远不会返回)。调度限制(限制并行性)通过相当奇怪的SubscribeOn()
扩展方法完成,这些方法是难以理解且难以正确使用的。如果开始学习Rx,请预留足够的时间来学习避免所有陷阱。但是,如果需要组合复杂的事件流或需要复杂的过滤/查询,则Rx确实是唯一的选择。我认为在微软发布mscorlib中的ISubject<T>
之前,Rx没有在广泛应用方面有机会。这很令人遗憾,因为Rx包含一些非常有用的具体(通用)类型,例如TimeInterval<T>
和Timestamped<T>
,我认为它们应该像Nullable<T>
一样在Core/mscorlib中。此外还有System.Reactive.EventPattern<TEventArgs>
。
我喜欢Scott W的项目符号。为了提供更多具体的例子,Rx非常适合:
TPL似乎很好地映射到:
我注意到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的第一章。
我认为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 数据流。