并行查询
System.Linq名称空间中包含的类ParallelEnuerable可以分解查询的工作,使其分布在多个线程上。尽管Enumerable类给IEnumerable<T>接口定义了扩展方法,但ParallelEnumerable类的大多数扩展方法是ParallelQuery<TSource>类的扩展。一个重要的例外是AsParallel()方法,它扩展了IEnumerable<TSource>接口,返回ParallelQuery<TSource>类,所以正常的集合类可以以并行方式查询。
为了说明PLINQ(Parallel LINQ,并行LINQ),需要一个大型集合。对于可以放在CPU的缓存中的小集合,并行LINQ看不出效果。在下面的代码中,用随机值填充一个大型的int集合:
static IEnumerable<int> SampleData()
{
const int arraySize = 50000000;
var random = new Random();
return Enumerable.Range(0,arraySize).Select(x=>random.Next(140)).ToList();
}
现在可以使用LINQ查询筛选数据,进行一些计算,获取所筛选数据的平均数。该查询用where子句定义了一个筛选器,仅汇总对应值小于20的项,接着调用聚合函数Average()方法。与前面的LINQ查询的唯一区别是,这次调用了AsParallel()方法。
static void LinqQuery(IEnumerable<int> data)
{
var res = (from x in data.AsParallel()
where Math.Log(x) < 4
select x).Average();
}
与前面的LINQ查询一样,编译器会修改语法,以调用AsParallel()、Where()、Select()和Average()方法。AsParallel()方法用ParallelEnumerable类定义,以扩展IEnumerable<T>接口,所以可以对简单的数组调用它。AsParallel()方法返回ParallelQuery<TSource>。因为返回的类型,编译器选择的Where()方法是ParallelEnumerable.Where(),而不是Enumerable.Where()。在下面的代码中,Select()和Average()方法也来自ParallelEnumerable类。与Enumerable类的实现代码相反,对于ParallelEnumerable类,查询是分区的,以便多个线程可以同时处理该查询。集合可以分为多个部分,其中每个部分由不同的线程处理,以筛选其余项。完成分区工作后,就需要合并,获得所有部分的工作汇总(本例是平均值)。
static void ExtensionMethods(IEnumerable<int> data)
{
var res = data.AsParallel()
.Where(x=>Math.Log(x) < 4)
.Select(x=>x).Average();
}
运行这行代码会启动任务管理器,这样就可以看出系统的所有CPU都在忙碌。如果删除AsParallel()方法,就不可能使用多个CPU。当然,如果系统上没有多个CPU,就不会看到并行版本带来的改进。
分区器
AsParallel()方法不仅扩展了IEnumerable<T>接口,还扩展了Partitioner类。通过它,可以影响要创建的分区。
Partitioner类用System.Collection.Concurrent名称空间定义,并且有不同的变体。Create()方法接受实现了IList<T>类的数组或对象。根据这一点,以及Boolean类型的参数loadBalance和该方法的一些重载版本,会返回一个不同的Partitioner类型。对于数组,使用派生自抽象基类OrderablePartitioner<TSource>的DynamicPartitionerForArray<TSource>类和StaticPartitionerForArray<TSource>类。
修改上面的示例代码,手工创建一个分区器,而不是使用默认的分区器:
static void UseAPartitioner(IList<int> data)
{
var result = (from x in Partitioner.Create(data,true).AsParallel()
where Math.Log(x) < 4
select x).Average();
}
也可以调用WithExecutionMode()和WithDegreeOfParallelism()方法来影响并行机制。对于WithExecutionMode()方法,可以传递ParallelExecutionMode的一个Default值或者ForceParallelism值。默认情况下,并行LINQ避免使用系统开销很高的并行机制。对于WithDegreeOfParallelism()方法,可以传递一个整数值,以指定应并行运行的最大任务数。查询不应使用全部CPU,这个方法会很有用。
取消
.NET提供了一种标准方式,来取消长时间运行的任务,这也适用于并行LINQ。
要取消长时间运行的查询,可以给查询添加WithCancellation()方法,并传递一个CancellationToken令牌作为参数。CancellationToken令牌从CancellationTokenSource类中创建。该查询在单独的线程中运行,在该线程中,捕获一个OperationCanceledException类型的异常。如果取消了查询,就触发这个异常。在主线程中,调用CancellationTokenSource类的Cancel()方法可以取消任务。
static void UseCancellation (IEnumerable<int> data) {
var cts = new CancellationTokenSource ();
Task.Run (() => {
try {
var res = (from x in data.AsParallel().WithCancellation(cts.Token)
where Math.Log(x) < 4
select x).Average();
System.Console.WriteLine($"query finished, overage: {res}");
} catch (OperationCanceledException ex){
System.Console.WriteLine(ex.Message);
}
}
);
System.Console.WriteLine("query started");
System.Console.Write("cancel?");
string input = Console.ReadLine();
if(input.ToLower().Equals("y")){
cts.Cancel();
}
}
来源:CSDN
作者:singhwong
链接:https://blog.csdn.net/qq_41708190/article/details/103736891