并行 PLINQ:
LINQ的基本功能就是对集合进行遍历查询,并在此基础上对元素进行操作。微软专门为 LINQ 拓展一个类 ParallelEnumerable (该类的命名空间也在 System.Linq 中),它所提供的扩展方法会让LINQ 支持并行计算,这就是所谓的 PLINQ。
ParallelEnumerable 类
ParallelEnumerable 类运算符 | 说明 |
---|---|
AsParallel | PLINQ 的入口点。 指定如果可能,应并行化查询的其余部分。 |
AsSequential | 指定查询的其余部分应像非并行的 LINQ 查询一样按顺序运行。 |
AsOrdered | 指定 PLINQ 应为查询的其余部分保留源序列的排序,或直到例如通过使用 orderby(在 Visual Basic 中为 Order By)子句更改排序为止。 |
AsUnordered | 指定保留源序列的排序不需要查询其余部分的 PLINQ。 |
WithCancellation | 指定 PLINQ 应定期监视请求取消时所提供的取消标记的状态以及取消执行。 |
WithDegreeOfParallelism | 指定 PLINQ 应用于并行化查询的处理器的最大数量。 |
WithMergeOptions | 提供有关 PLINQ 应如何(如果可能)将并行结果合并回使用线程上的一个序列的提示。 |
WithExecutionMode | 指定 PLINQ 应如何并行化查询(即使是当默认行为是按顺序运行查询时)。 |
ForAll | 一种多线程枚举方法,与循环访问查询结果不同,它允许在不首先合并回使用者线程的情况下并行处理结果。 |
Aggregate 重载 | 对于 PLINQ 唯一的重载,它启用对线程本地分区的中间聚合以及一个用于合并所有分区结果的最终聚合函数。 |
官方介绍:PLINQ
并行查询:
准备一个大型集合。对于可以放在 CPU 中的小型集合,并行 PLINQ 看不出效果。如下:用随机数填充一个大型集合
static IEnumerable<int> SampleData()
{
const int arraySize = 10000000;
var r = new Random();
return Enumerable.Range(0, arraySize).Select(x => r.Next(500)).ToList();
}
注意:我自己的电脑 arraySize = 1000000000 就会出现 OutOfMemoryException (内存溢出异常)。
现在可以使用并行进行数据处理:
求 集合中 数字的自然对数(底为 e)的值小于4 的元素 的 平均值
static void Main(string[] args)
{
List<int> data =(List<int>) SampleData();
//求 集合中 数字的自然对数(底为 e)的值小于4 的元素 的 平均值
Stopwatch sw = new Stopwatch();
sw.Start();
var res = (from x in data.AsParallel()
where Math.Log(x) < 4
select x
).Average();
sw.Stop();
Console.WriteLine("计算时间= "+ sw.ElapsedMilliseconds+" 毫秒");
Console.WriteLine("计算结果= "+res);
Console.ReadKey();
}
static IEnumerable<int> SampleData()
{
const int arraySize = 10000000;
var r = new Random();
return Enumerable.Range(0, arraySize).Select(x => r.Next(500)).ToList();
}
输出:
对于 ParallelEnumerable类,查询是分区的,以便于多个线程可以同时处理该查询。集合可以分为多个部分,其中每个部分有不同的线程处理,以筛选其余项。完成分区后就需要合并,获得所有部分的总和。
var res = data.AsParallel().Where(x => Math.Log(x) < 4).Select(x => x).Average();
运行上面这行代码会启动任务管理器,这样就可以看出CPU 使用率 飙升,CPU都在忙碌,如果删除 AsParallel 方法 就不可能使用多个CPU 。当然,如果系统没有多个CPU ,也就不会看到并行版本带来的改进。
运行前 CPU 利用率:6%
运行后CPU利用率:80%
分区器:
AsParallel() 方法不经扩展了 IEnumerable < T > 接口,还扩展了 Partitioner 类 。通过它,可以影响要创建的分区。
Partitioner 类: 提供针对数组、列表和可枚举项的常见分区策略。
命名空间: System.Collections.Concurrent ,且有不同的变体。官方详细链接:Partitioner
Partitioner类中的 Create() 方法 接受实现了 IList< T > 类的数组和对象。根据这一点,以及 Boolean 类型的参数 LoadBalance 和 该方法的一些重载版本,会返回一个不同的 Partitioner 类型。对于数组,.NET 4 及以上版本 包含派生自抽象基类 OrderablePartitioner< TSource > (表示将一个可排序数据源拆分成多个分区的特定方式。) 的 DynamicPartitionerForArray < TSource > 类和 StaticPartitionerForArray < TSource > 类。
OrderablePartitioner< TSource > 官方链接:OrderablePartitioner
PLINQ 和 TPL 的自定义分区程序:官方链接:自定义分区
修改上面的例子,手工创建一个分区,而不使用默认分区:
var res = (from x in Partitioner.Create(data, true).AsParallel()
where Math.Log(x) < 4
select x).Average();
也可以调用 WithExecutionMode() 【设置查询的执行模式。】和 WithDegreeOfParallellism() 【设置要在查询中使用的并行度。 并行度是将用于处理查询的同时执行的任务的最大数目】方法,来影响并行机制。
WithExecutionMode() Dome
static void Main(string[] args)
{
//表示线程安全的先进先出 (FIFO) 集合。
ConcurrentQueue<Penson> Pensons = new ConcurrentQueue<Penson>();
/*向集合中添加多条数据*/
Parallel.For(0, 6000000, (num) =>
{
Pensons.Enqueue(new Penson() { Address = "Address" + num, Name = "Name" + num, Age = num });
});
/*采用并行化整个查询 查询符合条件的数据*/
Stopwatch sw = new Stopwatch();
sw.Restart();
var personListLinq = from person in Pensons.AsParallel().WithExecutionMode(ParallelExecutionMode.ForceParallelism)
where (person.Name.Contains("1") && person.Name.Contains("2") && person.Address.Contains("1") && person.Address.Contains("2"))
select person;
Console.WriteLine("采用并行化整个查询 查询得出数量为:{0}", personListLinq.Count());
sw.Stop();
Console.WriteLine("采用并行化整个查询 耗时:{0}", sw.ElapsedMilliseconds);
/*采用默认设置 由.NET进行决策 查询符合条件的数据*/
sw.Restart();
var personListPLinq = from person in Pensons.AsParallel().WithExecutionMode(ParallelExecutionMode.Default)
where (person.Name.Contains("1") && person.Name.Contains("2") && person.Address.Contains("1") && person.Address.Contains("2"))
select person;
Console.WriteLine("采用默认设置 由.NET进行决策 查询得出数量为:{0}", personListPLinq.Count());
sw.Stop();
Console.WriteLine("采用默认设置 由.NET进行决策 耗时:{0}", sw.ElapsedMilliseconds);
Console.ReadKey();
}
class Penson
{
public string Name { get; set; }
public string Address { get; set; }
public int Age { get; set; }
}
参考博文链接:https://www.cnblogs.com/woxpp/p/3951096.html
WithDegreeOfParallellism() Dome:
static int NUM_INTS = 500000000;
static ParallelQuery<int> GenerateInputeData4Parallel()
{
return ParallelEnumerable.Range(1, NUM_INTS);
}
static void Main(string[] args)
{
var palTarget = GenerateInputeData4Parallel();
Console.WriteLine("============================================================");
Console.WriteLine("测试并行LINQ: 并行数量 = 2");
Console.WriteLine("============================================================");
var swatchp2 = Stopwatch.StartNew();
var palQuery = (from intNum in palTarget.AsParallel().WithDegreeOfParallelism(2)
where ((intNum % 5) == 0)
select (intNum / Math.PI)).Average();
swatchp2.Stop();
Console.WriteLine("PLINQ Result: " + palQuery + " LINQ Use Time: {0}", swatchp2.Elapsed);
palTarget = GenerateInputeData4Parallel();
Console.WriteLine("\n\n");
Console.WriteLine("============================================================");
Console.WriteLine("测试并行LINQ: 并行数量 = 4");
Console.WriteLine("============================================================");
var swatchp4 = Stopwatch.StartNew();
palQuery = (from intNum in palTarget.AsParallel().WithDegreeOfParallelism(4)
where ((intNum % 5) == 0)
select (intNum / Math.PI)).Average();
swatchp4.Stop();
Console.WriteLine("PLINQ Result: " + palQuery + " LINQ Use Time: {0}", swatchp4.Elapsed);
Console.ReadLine();
}
输出:
并行度不一样程序运行的时间差别还是很明显的。
参考博文链接:https://blog.csdn.net/wangzhiyu1980/article/details/46355633
WithExecutionMode() 方法可以传递 ParallelExecutionMode 的一个Default 值或 ForceParallelism值,默认情况下 并行 Linq 避免使用系统开销很高的并行机制。对于 WithDegreeOfParallellism() 方法 ,可以传递一个整数值,指定并行运行的最大任务数。查询不应使用全部的 CPU资源,这个方法很有用。
注意事项:
ParallelEnumerable 可以分解查询的工作,使其分布在多个线程上。尽管 Enumerable 类给 IEnumerable< T > 定义了扩展方法,但 ParallelEnumerable 类的大多数扩展方法是 ParallelQuery< TSource > 类的扩展。一个重要的例外就是 AsParallel() 方法,它扩展了 IEnumerable< TSource > 接口,返回 ParallelQuery< TSource > 类 所以正常的集合可以以并行的方式查询。
传统的Linq 计算是单线程,而PLinq 则是并发的多线程,如下实例:
static void Main(string[] args)
{
List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var query = from n in list select n;
Console.WriteLine("LINQ输出:");
foreach (var item in query)
{
Console.Write(" "+item.ToString());
}
Console.WriteLine();
Console.WriteLine("PLINQ输出:");
var quueryParallel = from n in list.AsParallel() select n;
foreach (var item in quueryParallel)
{
Console.Write(" "+item.ToString());
}
Console.ReadKey();
}
输出:
可以看出 Linq 是按照顺序输出,而 PLinq 输出是杂乱无章的。
并行输出还有另外一种方式可以处理 ,对 quueryParallel 求 ForAll:
quueryParallel.ForAll((n) =>
{
Console.Write(" "+n.ToString());
});
输出:
但是,如果要将并行输出后的结果进行排序,ForAll 会忽落掉 查询的 AsOrdered 请求。如下:
var quueryParallel = from n in list.AsParallel().AsOrdered() select n;
quueryParallel.ForAll((n) =>
{
Console.Write(" " + n.ToString());
});
AsOrdered() 方法可以对并行计算后的队列进行重新组合,以便于保持顺序。可是在 ForAll 方法中输出仍然是无须的。要保持 AsOrdered 方法的需求,我们应当始终使用第一中并行方式,即:
var quueryParallel = from n in list.AsParallel().AsOrdered() select n;
foreach (var item in quueryParallel)
{
Console.Write(" " + item.ToString());
}
在并行查询后再进行排序,会牺牲掉一定的性能。一些扩展方法默认会对元素进行排序,这些方法包括 : OrderBy ; OrderByDescending ; ThenBy 和 ThenByDescending 。在实际的使用中,一定要注意各种方式之间的差别。以便程序按照我们的设想运行。
还有其他的查询方法,比如 Take。 如果我们这样编码:
foreach (var item in quueryParallel.Take(5))
{
Console.Write(" " + item.ToString());
}
在顺序查询中会返回前5个元素,但在 PLINQ 中会返回 5个无序的元素。
建议在对集合元素进行操作的时候使用 PLINQ 代替 LINQ 。但要记住,不是所有的并行查询速度都会比顺序查询快,在对集合执行某些方法时,顺序查询的速度会更快一些,比如方法 ElementAt等。在开发中,应仔细辨别这方面的要求,以便于找到最佳的解决方案。