基于任务的并发编程之Parallel.Invoke(一)

时间:2021-08-21 02:21:28

基于任务的并发编程之Parallel.Invoke(一)

         如果想并行执行很多方法,最便捷的方式就是使用Parallel类提供的新方法Invoke方法。例如,假设你有下边四个独立的方法,每个方法执行一个格式化转换,并且你们确定并行执行它们是安全的。

Ø ConvertEllipses

Ø  ConvertRectangles

Ø  ConvertLines

Ø  ConvertText

为了启动这些无参返回值为空的函数时充分利用潜在的并行优势,你可以使用下边这行代码:

Parallel.Invoke(ConvertEllipses,ConvertRectangles,ConvertLines,ConvertText);

在这种情况下,这行代码将会创建指向每个方法的委托。Invoke方法的定义接收一个Action类型的参数数组并并行执行它们。

下面使用拉姆达表达式执行这些方法的代码也会产生同样的结果。

Parallel.Invoke(() =>ConvertEllipses(),() => ConvertRectangles(),() => ConvertLines(),() =>ConvertText());

就像下面代码例子所展示的,你也可以使用拉姆达表达式和匿名委托来执行这些方法。

 

Parallel.Invoke(
() =>
{
ConvertEllipses();
//  Do something else adding more lines
},
() =>
{
ConvertRectangles();
//  Do something else adding more lines
},
delegate()
{
ConvertLines();
//  Do something else adding more lines
},
delegate()
{
ConvertText();
});

没有特定的执行顺序

         下面的解释适用于前面展示的任何一个代码清单。前面代码清单中的Parallel.Invoke只有在四个方法都执行完以后才会返回。然而,完成也包括抛出异常的情况。

         这个方法会尽力尝试同时启动这四个方法,以便充分的利用一个或者多个物理微处理器提供的多个逻辑核心的优势。然而,真正的实现它们的并行执行会依赖很多的因素。在这个场景中,有四个方法。这意味着Parallel.Invoke需要至少四个逻辑核心来实现并行的执行四个方法。逻辑核心,也就是众所周知的软件线程。记住理解运行你的并行代码的新型多核硬件是很重要的。

         有四个核心并不能保证四个方法就是同时启动的。如果一个或者多个核心太繁忙,底层的调度逻辑就会延迟初始化提供的某些方法的执行。确实是很难精确的预测执行的顺序,因为底层的逻辑将会实时根据可用的资源,动态的创建最合适的执行计划。

         2-4展示了在不同的硬件配置或者不同的负载下,可能发生的三种不同的并发执行场景。注意相同的代码并不是总是执行一段固定的时间;因此,即使使用相同的硬件配置和数据输入流,ConvertText方法可能比ConvertLines方法占用更长的时间。

         第一幅图中展示了一种几乎理想的情形:四个方法并行执行。考虑在必要的时间调度这些并行的任务是很重要的,这增加了初始化的时间开销。

         第二幅画展示了一个不同的场景。这里仅仅有两个并发的车道,但是却需要执行四个方法。在一条车道上,当ConvertEllipses结束的时候,ConvertRectangles就开始执行。在另外一条车道上,当ConvertLines结束的时候,ConvertText就开始执行。此时Parallel.Invoke执行所有的方法比前面的场景会花费更长的时间。

         第三个图中展示了有三个车道的另外一个场景。然而,这种情况使用的时间几乎跟第二个图中的一样,因为ConvertLines方法占用了更长的执行时间。因此,即使多使用了一个额外的并行车道,Parallel.Invoke执行所有的方法占用的时间与前边第二种场景基本相同。

基于任务的并发编程之Parallel.Invoke(一)

2-4

如果你在一个至少有两个物理核心的计算机上多次执行在清单2-1中展示的控制台程序,就很容易理解这个执行顺序的问题。

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Listing2- 1
{
class Program
{
static  void Main( string[] args)
{
Parallel.Invoke(
() => ConvertEllipses(),
() => ConvertRectangles(),
() => ConvertLines(),
() => ConvertText());
System.Console.ReadLine();
}
static  void ConvertEllipses()
{
System.Console.WriteLine(“Ellipses converted.”);
}
static  void ConvertRectangles()
{
System.Console.WriteLine(“Rectangles converted.”);
}
static  void ConvertLines()
{
System.Console.WriteLine(“Lines converted.”);
}
static  void ConvertText()
{
System.Console.WriteLine(“Text converted.”);
}
}
}

例如,考虑如下Parallel.Invke的串行版本代码

ConvertEllipses();

ConvertRectangles();

ConvertLines();

ConvertText();

就像这里展示的,在不同的多核计算机的控制台上输出的结果是相同的,因为代码的执行顺序是相同的。

Ellipses converted.

Rectangles converted.

Lines converted.

Textconverted.

         然而,如果你执行Parallel.Invoke版本代码,即使哦那个样的硬件配置,执行的结果往往是不同的。清单2-2展示了在同一台多核计算机上执行三次在控制台输出的结果。在输出结果中那些高亮显示的是与串行代码中顺序相同的。

基于任务的并发编程之Parallel.Invoke(一)

Listing2-2

         第一次的时候,并行代码产生了与串行版本相同的输出结果。然而,第二次和第三次,执行的顺序是不同的。例如,第三次,rectangles的输出是最后出现的,而不是第二个出现的。这个简单的例子展示了串行代码和并行代码的一个不同点。