响应式框架、PLINQ、TPL和并行扩展如何相互关联?

70

至少自.NET 4.0发布以来,微软似乎在支持并行和异步编程方面投入了大量精力,很多围绕这方面的API和库出现了。特别是最近到处都提到以下几个花哨的名字:

  • 反应式框架(Reactive Framework),
  • PLINQ(Parallel LINQ),
  • TPL(Task Parallel Library)和
  • Parallel Extensions。

它们似乎都是微软产品,且都针对.NET的异步或并行编程场景。但每个产品实际上是什么以及它们之间的关系并不十分清晰。有些可能实际上是同一件事情。

简单地说,有人能不能澄清一下各个方面的情况呢?


我已经编辑了这个问题,试图澄清一下,并投票重新开放它。话虽如此,我不是一个 .NET 专家,所以我可能把它搞砸了;如果你认为我在某个地方犯了错误,请随意进一步编辑它(甚至完全恢复我的更改)。另外,坦率地说,我仍然认为这个问题“感觉”过于广泛和不清楚,不适合在 Stack Overflow 上很好地解决;话虽如此,鉴于它确实有一个高度赞成和接受的答案,我愿意接受它可能是一个例外。 - Ilmari Karonen
我认为所有知道上述4件事情的人(也就是潜在回答这个问题的每个人)都会立刻知道这个问题的意思。点赞和回答也证明了这一点。无论如何,我已经编辑了这个问题,使其尽可能清晰明了。 - bitbonk
2个回答

99

PLINQ(Parallel Linq)是一种编写常规Linq查询的新方法,以便并行运行-换句话说,框架将自动处理跨多个线程运行查询,从而使它们更快地完成(即使用多个CPU核心)。

例如,假设您有一堆字符串,您想获取所有以字母"A"开头的字符串。您可以像这样编写查询:

var words = new[] { "Apple", "Banana", "Coconut", "Anvil" };
var myWords = words.Select(s => s.StartsWith("A"));

这很好用。但是,如果您要搜索50000个单词,您可能需要利用每个测试是独立的事实,并将其分配到多个核心上:

var myWords = words.AsParallel().Select(s => s.StartsWith("A"));

这就是将普通查询转化为运行在多个核心上的并行查询所需要做的一切,非常不错。


TPL(任务并行库)可以看作是 PLINQ 的补充,它们一起组成了 Parallel Extensions。虽然 PLINQ 大部分基于无副作用的函数式编程,但正是 TPL 用来处理副作用。如果你想实际并行执行工作,而不仅仅是并行搜索/选择内容,则需要使用 TPL。

TPL 实际上就是暴露了 For, Foreach, 和 InvokeParallel 类。像 Invoke 就有点像在 ThreadPool 中排队任务,但使用起来更简单。在我看来,更有趣的是 ForForeach 部分。例如,假设你有一堆文件要压缩,你可以编写常规的顺序版本:

string[] fileNames = (...);
foreach (string fileName in fileNames)
{
    byte[] data = File.ReadAllBytes(fileName);
    byte[] compressedData = Compress(data);
    string outputFileName = Path.ChangeExtension(fileName, ".zip");
    File.WriteAllBytes(outputFileName, compressedData);
}

同样,这个压缩算法的每一次迭代都是完全独立于其他迭代的。我们可以通过同时进行多次迭代来加速此过程:

Parallel.ForEach(fileNames, fileName =>
{
    byte[] data = File.ReadAllBytes(fileName);
    byte[] compressedData = Compress(data);
    string outputFileName = Path.ChangeExtension(fileName, ".zip");
    File.WriteAllBytes(outputFileName, compressedData);
});
再次强调,这就是并行化此操作所需的全部内容。现在当我们运行CompressFiles方法(或我们决定如何称呼它)时,它将使用多个CPU核心,并可能在一半或四分之一的时间内完成。
与仅将所有内容扔到ThreadPool中相比,这种方法的优势在于它实际上是同步运行的。如果使用ThreadPool(或只是普通的Thread实例),您必须想出一种找出所有任务何时完成的方法,虽然这并不是非常复杂,但很多人往往会弄错或至少有困难。使用Parallel类时,您不必考虑这些; 多线程方面对您隐藏了,所有这些都在幕后处理。 响应式扩展(Rx)实际上是完全不同的东西。这是一种不同的事件处理方式。确实有很多关于这个的资料需要涵盖,但简而言之,与将事件处理程序连接到事件不同,Rx允许您将事件序列视为......好吧,序列(IEnumerable<T>)。您可以以迭代方式处理事件,而不是在随机时间异步触发它们,您必须一直保存状态才能检测特定顺序中发生的一系列事件。
我发现Rx最酷的例子之一是这里。跳转到“ Linq to IObservable”部分,他在仅使用4行代码的情况下实现了通常在WPF中很难处理的拖放处理程序。 Rx为您提供了事件的组合,这是您在常规事件处理程序中真正没有的内容,并且像这样的代码片段也容易重构为行为类,您可以将其套用在任何地方。
就是这样。这些是.NET 4.0中可用的一些更酷的功能。当然还有其他几个,但这些就是你问的!

这些是否在 .net 3.5 或更低版本中可用? - Myster
1
@Myster:RX 已经发布(http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx)。PLINQ 早在长时间之前就作为CTP发布了,但现在不再提供 - 若要使用请升级。 - Aaronaught
2
TPL数据流库(TDF)是随着C#异步CTP一起推出的,看起来非常不错。它应该使得.NET中间件更容易保持在并行处理的“性能甜点”之内。为了获得最佳性能(考虑吞吐量和延迟),您需要设置限制。因此,每个处理“块”都可以有自己的“缓冲区容量”和并行度上限。您还可以告诉每个块处理批次而不是单个消息。这看起来很棒。 - yzorg
现在考虑使用渠道是明智的选择:https://learn.microsoft.com/en-us/dotnet/core/extensions/channels,它似乎比数据流库更高效。 - Loudenvier

31
我喜欢Aaronaught的答案,但我认为Rx和TPL解决了不同的问题。TPL团队添加的部分是线程原语和对运行时构建块的显着增强,例如线程池。您列出的所有内容都是基于这些原语和运行时功能构建的。
但TPL和Rx解决了两个不同的问题。当程序或算法正在“拉取和排队”时,TPL效果最佳。当程序或算法需要从流中(例如鼠标输入或从像WCF这样的终结点接收到的相关消息流)“反应”数据时,Rx表现出色。
要执行文件系统、遍历集合或走访组织图表等工作,您需要来自TPL的“工作单元”概念。在这些情况下,程序员可以思考整体工作量,将工作量分解为特定大小的块(任务),而且在对层次结构进行计算时,任务可以“链接”在一起。因此,某些类型的工作适合TPL的“任务层次结构”模型,并受益于管道的增强,例如取消(请参见有关CancellationTokenSource的Channel 9视频)。TPL还具有许多针对专业领域的旋钮,例如近实时数据处理。
Rx将成为大多数开发人员最终使用的东西。这就是WPF应用程序如何“反应”对外部消息的方式,例如外部数据(IM客户端中的IM消息流)或外部输入(例如来自Aaronaught链接的鼠标拖动示例)。在内部,Rx使用来自TPL/BCL的线程原语、线程安全集合以及运行时对象,例如线程池。在我看来,Rx是表达您意图的“最高级别”的编程方式。
普通开发人员是否能够理解您可以使用Rx表达的意图尚有待观察。 :)
但我认为未来几年TPL与Rx之间的争论将成为下一个LINQ-to-SQL vs. Entity Framework的辩论。这是同一领域中两种API的不同版本,并专门针对不同的场景进行了优化,但在很多方面存在重叠。但在TPL和Rx的情况下,它们实际上彼此知道,并且有内置适配器来组合应用程序并一起使用这两个框架(例如将PLINQ循环的结果馈送到IObservable Rx流中)。对于那些没有进行任何并行编程的人来说,需要大量的学习才能跟上速度。
更新:在我的常规工作中,过去6个月中我已经使用了TPL和RxNet(自18个月前我最初的回答以来)。我在中间层WCF服务(企业LOB服务)中选择TPL和/或RxNet的想法:http://yzorgsoft.blogspot.com/2011/09/middle-tier-tpl-andor-rxnet.html

1
另一个需要注意的是TPL和TPL Dataflow默认启用并行处理。Rx默认情况下以“丰富事件”开始,因此它会让您跳过一些步骤来进行并行处理(例如增加并行处理的并发项数)。请参见“将IObservable视为线程安全”和“如何使用Rx异步处理队列”。在我看来,使用阻塞操作意外地很容易在Rx中发生。 - yzorg

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