并行编程之PLINQ
并行 LINQ (PLINQ) 是 LINQ 模式的并行实现。PLINQ 的主要用途是通过在多核计算机上以并行方式执行查询委托来加快 LINQ to Objects 查询的执行速度。与顺序 LINQ 查询一样,PLINQ 查询对任何内存中 IEnumerable 或 IEnumerable<(Of <(T>)>) 数据源进行操作,并推迟执行,这意味着在枚举查询之前不会开始执行这些操作。主要区别是 PLINQ 尝试充分利用系统中的所有处理器。它利用所有处理器的方法是,将数据源分成片段,然后在多个处理器上对单独工作线程上的每个片段并行执行查询。在许多情况下,并行执行意味着查询运行速度显著提高。
通过并行执行,PLINQ 通常只需向数据源添加 AsParallel 查询操作,即可在某些查询类型的旧版代码上获得显著的性能改进。但是,并行可能引入其自己的复杂性,因此并非所有查询操作在 PLINQ 中都运行得更快。事实上,并行降低了某些查询的速度。
System.Linq..::.ParallelEnumerable 类公开 PLINQ 的几乎所有功能。此类和 System.Linq 命名空间类型的其余部分一起编译到 System.Core.dll 程序集中。
1、选择使用模型
当您编写查询时,通过在数据源上调用 ParallelEnumerableAsParallel()()() 扩展方法来选择使用 PLINQ,如下面的示例所示。
var source = Enumerable.Range(1, 10000);
// Opt-in to PLINQ with AsParallel
var evenNums = from num in source.AsParallel()
where Compute(num) > 0
select num;
2、并行度
默认情况下,PLINQ 使用主机上的所有处理器,这些处理器的数量最多可达 64 个。通过使用 WithDegreeOfParallelism()()() 方法,可以指示 PLINQ 使用不多于指定数量的处理器。这在当您要确保计算机上运行的其他进程收到一定的 CPU 时间量时非常有用。下面的代码段将查询限制为最多使用两个处理器。
var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
where Compute(item) > 42
select item;
3、总体工作的计算开销
为了实现加速,PLINQ 查询必须具有足够的适合并行工作来弥补开销。工作可表示为每个委托的计算开销与源集合中元素数量的乘积。假定某个操作可并行化,则它的计算开销越高,加速的可能性就越大。例如,如果某个函数执行花费的时间为 1 毫秒,则针对 1000 个元素进行的顺序查询将花费 1 秒来执行该操作,而在四核计算机上进行的并行查询可能只花费 250 毫秒。这样就产生了 750 毫秒的加速。如果该函数对于每个元素需要花费 1 秒来执行,则加速将为 750 秒。如果委托的开销很大,则对于源集合中的很少几个项,PLINQ 可能会提供明显的加速。相反,包含无关紧要委托的小型源集合通常不适合于 PLINQ。
在下面的示例中,queryA 可能适合于 PLINQ(假定其 Select 函数涉及大量工作)。queryB 可能不适合,原因是 Select 语句中没有足够的工作,并且并行化的开销将抵销大部分或全部加速。
var queryA = from num in source.AsParallel()
select ExpensiveFunction(num); //good for PLINQ
var queryB = from num in source.AsParallel()
where num % 2 > 0
select num; //not as good for PLINQ
4、PLINQ 何时选择顺序模式
PLINQ 将始终尝试至少按与查询以顺序方式运行同样的速度来执行查询。尽管 PLINQ 不会查看用户委托的计算开销有多高或输入源有多大,但它却会查找某些查询"形状"。具体而言,它将查找通常会导致查询在并行模式下运行更慢的查询运算符或运算符组合。如果找到此类形状,PLINQ 默认情况下会转而使用顺序模式。
但是,在衡量特定查询的性能后,您可能会确定该查询在并行模式下实际运行更快。在这些情况下,您可以通过 ParallelEnumerableWithExecutionMode()()() 方法使用 ParallelExecutionMode..::.ForceParallelism 标志,以指示 PLINQ 对查询进行并行化。有关更多信息,请参见如何:在 PLINQ 中指定执行模式。
下面的列表介绍了 PLINQ 默认情况下将按顺序模式执行的查询形状:
- 包含 Select 子句、已建立索引的 Where 子句、已建立索引的 SelectMany 子句或 ElementAt 子句的查询(在排序或筛选运算符移除或重新排列了索引后)。
- 包含 Take、TakeWhile、Skip、SkipWhile 运算符并且源序列中的索引未采用原始顺序的查询。
- 包含 Zip 或 SequenceEquals 的查询,除非其中一个数据源具有按原始顺序排列的索引,并且另一个数据源可建立索引(即,数组或 IList(T))。
- 包含 Concat 的查询,除非将其应用到可建立索引的数据源。
- 包含 Reverse 的查询,除非应用到可建立索引的数据源。
5、PLINQ 中的顺序保留
下面的示例演示一个未排序的并行查询,该查询筛选与某个条件匹配的所有元素,而不会尝试以任何方式对结果进行排序。此查询不一定会生成源序列中满足条件的前1000 个城市,而是会生成满足条件的某一组 1000 个城市。您可以通过对源序列使用 AsOrdered()()() 运算符来启用顺序保留。然后,您可以稍后通过使用 AsUnOrdered()()() 方法在查询中禁用顺序保留。
var cityQuery = (from city in cities.AsParallel()
where city.Population > 10000
select city)
.Take(1000);
查询运算符和排序
下面的查询运算符将顺序保留引入查询的所有后续运算中,或直至调用 AsUnordered()()() 为止:
- OrderBy()()()
- OrderByDescending()()()
- ThenBy()()()
- ThenByDescending()()()
下面的 PLINQ 查询运算符在某些情况下可能要求经过排序的源序列生成正确的结果:
- Reverse()()()
- SequenceEquals()()()
- TakeWhile()()()
- SkipWhile()()()
- Zip()()()