c# 高级编程 12章267页 【LINQ】【并行LINQ】

136 阅读2分钟

ParallelEnumerable 类

  • 包含在System.Linq命名空间中
  • 分解查询工作,使其分布在多个线程上
  • 查询是分区的,以便多个线程可以同时处理该查询,分区的工作各自完成后,需要合并
  • 它的大多数扩展方法ParallelQuery<TSource>的扩展方法

AsParallel() 方法

  • ParallelEnumerable类中定义
  • 它是IEnumerable<TSource>的扩展方法
    • 因此,正常的集合类都可以以并行方式查询
  • 它也是Partitioner类的扩展方法,可以影响要创建的分区
  • 它返回ParallelQuery<TSource>
    • 在此基础上调用的Where(),不是Enumerable.Where()
    • 而是ParallelEnumerable.Where()
  • 观察AsParallel()生效的方式: 系统所有CPU都在忙碌。如果去掉它,就不可能使用多个CPU

并行查询

对于可以放在CPU缓存中的小集合,并行LINQ看不出效果

对于没有多个CPU的系统,并行LINQ也没有效果 先构造一个大集合:

        static IList<int> SampleData()
        {
            const int arraySize = 50000000;
            var r = new Random();
            return Enumerable.Range(0, arraySize).Select(x => r.Next(140)).ToList();
        }

并行查询,使用AsParallel()方法:

  • 子句语法的LINQ查询一样,编译器会修改它,以实际调用
    • ParallelEnumerableAsParallel()
    • ParallelEnumerableWhere()
    • ParallelEnumerableSelect()
    • ParallelEnumerableAverage()
        static void LinqQuery(IEnumerable<int> data)
        {
            var res = (from x in data.AsParallel()
                       where Math.Log(x) < 4
                       select x).Average();
        }
        static void ExtensionMethods(IEnumerable<int> data)
        {
            var res = data.AsParallel()
                .Where(x => Math.Log(x) < 4)
                .Select(x => x).Average();
        }

分区器: Partitioner 类

  • System.Collection.Concurrent 命名空间
  • 它的Create()方法,接收实现了IList<T>的类型实例
        static void UseAPartitioner(IList<int> data)
        {
            var res = (from x in Partitioner.Create(data, loadBalance: true).AsParallel()
                       where Math.Log(x) < 4
                       select x).Average();

        }

数组专用的分区器:

  • DynamicPartitionerForArray<TSource>StaticPartitionerForArray<TSource>
    • 它们继承自抽象基类OrderablePartitioner<TSource>

其他方法影响并行机制:

  • WithExecutionMode()
    • 可以传递一个ParallelExecutionMode的默认值。(默认情况下,并行LINQ避免使用系统开销很高的并行机制
    • 或者,可以传递ForceParallelism
  • WithDegreeOfParallelism()
    • 可以传递一个整数值,指定并行运行的最大任务数
    • 当查询不应该使用全部CPU时,这个方法会很有用

取消

要取消长时间运行的任务,可以给查询添加WithCancellation()方法,并传递一个CancellationToken令牌作为参数

  • CancellationTokenCancellationTokenSource中创建
  • 当要取消任务时:
    • 主线程中,调用CancellationTokenSource类的Cancel()方法
    • 在查询运行的线程中,查询会中止,然后抛一个OperationCanceledException异常。我们需要捕获这个异常
static void UseCancellation(IEnumerable<int> data)
        {
            Console.WriteLine(nameof(UseCancellation));
            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();

                    Console.WriteLine($"query finished, sum: {res}");
                }
                catch (OperationCanceledException ex)
                {
                    Console.WriteLine(ex.Message);
                }
            });

            Console.WriteLine("query started");
            Console.Write("cancel? ");
            string input = Console.ReadLine();
            if (input.ToLower().Equals("y"))
            {
                cts.Cancel();
            }

            Console.WriteLine();
        }