65.9K
CodeProject 正在变化。 阅读更多。
Home

并行 .NET 4.0 入门

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (33投票s)

2010年11月30日

CPOL

8分钟阅读

viewsIcon

76018

.NET 4.0 并行特性入门 - PLINQ、TPL 和 Rx

引言

过去,处理器只有一个核心,时钟频率每年都在稳步提升。这意味着,虽然为单个 CPU 编写多线程代码可以带来一些性能提升(因为单核 CPU 通常会尝试执行多个线程),但它几乎不是任何开发者的首要任务。在过去的几年里,时钟频率已经达到了平台期,我们都拥有双核或四核处理器,并且核心数量增长的趋势将继续下去。

对并行化冷漠的原因之一是,必须付出相当大的努力来确保线程安全,并避免死锁和竞态条件等问题,而相关的维护成本和引入错误的范围往往会超过性能的提升。在这篇文章中,我将介绍 .NET 4.0 提供的一些特性,以帮助我们通过利用并行化的机会来编写更具可伸缩性的代码,因为我们正朝着多核和众核处理器的世界迈进。

题外话:“众核”(Many-core)描述了一个包含比正在运行的进程数更多核心的处理器,数量在数百或数千个核心的范畴。正是在这一点上,软件并行化至关重要;否则,系统将无法高效运行,因为核心数量多于线程数。如果您认为这个核心数量是完全未来化的,那就错了——NVIDIA 早已因渲染引擎对并行化的需求而大力推广其并行处理器。NVIDIA GTX280 GPU 拥有 240 个核心,而 Tesla M20 拥有 448 个核心 [1]。英特尔也宣布了一款名为“Knights Corner”的 50 核 CPU [2]。

在我们使用应用程序中的并发性时,我们开发者经常会编写针对特定核心数量的代码。例如,我想通过在后台执行一个长时间运行的计算来释放我的 UI 线程。因此,很自然地,我会在线程池中的一个新线程中运行我的计算。但是,如果我的处理器有四个核心呢?或者 1000 个呢?我已经将我的软件硬编码为只使用两个线程。这是加州大学伯克利分校 2006 年一份题为《并行计算研究格局:伯克利视角》的报告 [3] 中强调的主要问题,其中指出:

“总体的目标应该是让编写的程序在高度并行的计算系统上高效执行变得容易”

和 -

“要取得成功,编程模型应独立于处理器数量。”

不过,不用担心——这些正是 .NET 4.0 通过将开发人员与底层线程问题分开来解决的问题。从现在开始,重点是 什么 在并行运行,而不是 如何 运行。通过这种方式,我们可以更轻松地为上述众核场景的软件做好未来规划。Px (Parallel Extensions) 是 .NET 4.0 并行特性的总称。它包括 **Task Parallel Library (任务并行库)** 和 **PLINQ**。下面,我将简要介绍它们各自的概况,并作为额外福利,介绍 **Rx – Reactive Extensions(响应式扩展)**——它构建在 Px 之上。

任务并行库 [4]

该库包含(但不限于)并行化 for foreach 循环的函数,并引入了 Task 类型,该类型可以替代我们对 ThreadPool 的使用。Task 类型用于表示一个操作,该操作可能花费一些时间等待,而不会阻塞其线程。

  • Parallel.For Parallel.ForEach :为循环体创建 Task 委托,允许并行调用。
  • Parallel.Invoke :允许并行调用多个语句。
  • Task.WaitAll :仅在等待所有任务完成时才阻塞主线程,而不是阻塞每个单独的线程。
  • Task.Factory.ContinueWhenAll :创建一个表示多个子任务完成的任务。
警告 - 对于一开始就很简单的代码,不要过度热衷于将您漂亮的并行算法转换为使用并行构造。由于同步(用于线程负载均衡)和运行 Task 所涉及的委托调用存在(微小)开销,您可能会适得其反。

PLINQ [5]

Parallel LINQ 是一组扩展方法,可用于并行执行您通常的 LINQ 查询。还包含一些不存在于老式串行 LINQ 中的附加扩展。这些扩展方法存在于 ParallelQuery<T> 类型上,您可以通过在 IEnumerable 对象上调用 AsParallel() 来获取其实例。我将在此介绍一些扩展方法,以让您对 PLINQ 有一些感觉。

  • Select, Where, OrderBy, Aggregate, All 等:标准运算符的并行版本。
  • ForAll :对 ParallelQuery 集合中的每个项执行一个操作。当然是并行的。
  • AsOrdered :指示并行查询应被视为有序的——因此,当后续执行并行操作时,您可以确保生成的集合以相同的顺序输出。
  • AsUnordered :指示 ParallelQuery 不需要有序——这默认是这种情况。
  • AsSequential :将 ParallelQuery 转换为 IEnumerable,以强制后续操作进行顺序评估。
  • WithCancellation :允许传入一个取消令牌,以便并行操作可以被取消。
  • WithExecutionMode :PLINQ 有时会判断顺序运行查询更有效率。但是,如果您比 PLINQ 更了解,可以强制它始终并行运行。
警告:在用这些并行版本替换查询时,您想要并行执行的所有内容都必须是独立的。请小心,不要让您的 lambda 表达式包含任何副作用,并且实际上是可并行的!参阅进一步阅读。:-)

Rx [6]

Rx (Reactive Extensions,响应式扩展) 就像 LINQ 的一次重现,但方向相反。LINQ 是 IEnumerable 的一组扩展方法,而 Rx 是 IObservable 的一组扩展方法。这是因为 IObservable IEnumerable’s 的数学对偶——其中 IEnumerable 集合是一个“拉取”(pull)集合,而 IObservable 是一个“推送”(push)集合。它们是相等且相反的。与使用 IEnumerable 逐个请求集合中的项不同,IObservable 会在每次收到新项时向您发送一个通知,并在到达集合末尾时发送另一个通知。

等等——这一切与并行有什么关系?嗯,Rx 与可用的并行构造相辅相成,因为它允许我们组合异步源来构建我们的应用程序。这些异步源可以是来自某个独立并行计算的输出——或者更基础的东西,如 UI 事件(在不同的线程上)。它为我们处理了跨线程问题所涉及的所有复杂性,使我们能够专注于组合。此外,Rx 依赖于 Px,因为它的底层操作已经使用对并行友好的技术来实现。

在接下来的示例中,IObservable 被称为源。然后,您在该源上调用 Subscribe ,并传入一些 Action 来处理您希望对每个项、在结束时以及在出错时执行的操作。例如,首先创建一个空集合

IObservable<int> source = Observable.Empty<int>(); 

创建源后,您必须订阅它以接收通知。这是通过 Subscribe 方法完成的

IDisposable subscription = source.Subscribe(
    item => {}, //Do something with each item
    ex => {}, //Handle an item exception
    () => {} //Do something on completion
);

使用上面创建的空源,我们只会收到一个 OnCompleted 通知。显然,空源没什么用——以下是一些您可能想要创建 IObservable 的情况:

  • 您可以从一个长时间运行的计算创建 IObservable 集合,并编写代码以异步地处理每个新结果。
  • 您可以将用户输入视为一个可观察的集合。例如——鼠标移动事件实际上只是一个永无止境的项集合。
  • 任何其他 .NET 事件都可以通过使用 Observable.FromEvent 方法来实例化 IObservable

许多 LINQ 扩展方法都可用——Where, Select, Skip——它们执行的操作与您在 LINQ 中习惯的操作相同。这是一个来自 Rx Hands On Labs 白皮书 [6] 的有趣的 IObservable 源示例,如果您打算在代码中使用 Rx,我强烈建议您阅读它。

var src = Observable.FromEvent<EventArgs>(textBox, "TextChanged") //Listen to 
						//textbox TextChanged event
    .Select(evt => ((TextBox)evt.Sender).Text) //Get the text
    .Throttle(TimeSpan.FromSeconds(1)) //Only receive max one per second
    .DistinctUntilChanged(); //Ignore contiguous duplicates

//Call ObserveOn to synchronise with the UI thread, and write to a label.
using(src.ObserveOn(label).Subscribe(
    txt => 
    {
        label.Text = txt; 
    })
{ 
    Application.Run(frm); 	//When the form is closed, the 'using' block exits, 
			//and the subscription is disposed.
}

上面提到的白皮书示例将这进一步深化——通过将项发送到字典webservices(它还内置了对异步 Web 服务的支持),接收响应,调用 SelectMany 将单个输入映射到多个结果,并将它们转储到 ListView 中。所有这些都只需几行代码,并避免了我们习惯的事件处理器的混乱。

结论

这篇文章的目的是介绍 .NET 4.0 中可用的部分并行特性,我已尽力避免深入太多细节。例如,它支持并行执行操作期间的异常处理、显式指定后台创建的任务(线程)数量以及任务调度,所有这些在实际应用程序中可能都需要用到。此外,还有许多更基础的功能我已省略。

我希望您现在能理解 .NET 中对并行支持的需求和可用性。如果上面描述的任何特性让您感兴趣,请查看下面的进一步阅读链接。我强烈推荐这样做,因为我认为这很可能成为大多数 .NET 开发人员未来的重要组成部分。

祝好,

Johnny

延伸阅读

  1. NVIDIA 多核 GPU – Tesla 规格 http://www.nvidia.com/object/product_tesla_c1060_us.html
  2. Intel “Knights Corner” 50 核芯片新闻稿 http://www.intel.com/pressroom/archive/releases/20100531comp.htm
  3. 并行计算研究格局:伯克利视角http://www.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-183.pdf
  4. 并行编程模式:使用 .NET Framework 4 理解和应用并行模式http://www.microsoft.com/downloads/en/details.aspx?FamilyID=86B3D32B-AD26-4BB8-A3AE-C1637026C3EE
  5. PLINQ 入门 http://msdn.microsoft.com/en-us/library/dd997425.aspx
  6. Rx .NET 实操实验 http://download.microsoft.com/download/C/5/D/C5D669F9-01DF-4FAF-BBA9-29C096C462DB/Rx%20HOL%20.NET.pdf

历史

 

  • 2010 年 11 月 30 日:初始版本
© . All rights reserved.