并行LINQ

我们两清 提交于 2020-01-15 04:21:35

并行查询 

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();
            }
        }

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!