Введение в PLINQ
Параллельный запрос
LINQ был представлен в .NET Framework версии 3.0. В нем используется единая модель запроса любого источника данных System.Collections.IEnumerable или System.Collections.Generic.IEnumerable<T> типобезопасным образом. LINQ to Objects — это имя запросов LINQ, которые выполняются в коллекциях в памяти, например List<T> и массивы. В данной статье предполагается, что пользователь имеет базовые знания по LINQ. Дополнительные сведения см. в разделе LINQ.
Параллельный LINQ (PLINQ) является параллельной реализацией шаблона LINQ. Запрос PLINQ во многом напоминает непараллельный запрос LINQ to Objects. Запросы PLINQ, так же как последовательные запросы LINQ, работают в любом источнике данных IEnumerable или IEnumerable<T> в памяти и имеют возможность отложенного выполнения, т. е. они не начинают выполняться, пока запрос не перечислен. Основным различием является то, что PLINQ пытается полностью использовать возможности всех процессоров в системе. Это достигается путем разделения источника данных на сегменты и параллельного выполнения запроса каждого сегмента в отдельном рабочем потоке на нескольких процессорах. Во многих случаях параллельное выполнение означает, что запрос выполняется значительно быстрее.
Благодаря параллельному выполнению PLINQ может значительно увеличить производительность по сравнению с устаревшим кодом для определенных типов запросов часто просто путем добавления операции запроса AsParallel к источнику данных. Однако параллелизм может привести к появлению собственных сложностей, и не все операции запросов выполняются быстрее в PLINQ. В действительности, параллелизация фактически замедляет выполнение определенных запросов. Таким образом, следует понимать влияние различных проблем, например упорядочения, на параллельные запросы. Дополнительные сведения см. в разделе Общее представление об ускорении выполнения в PLINQ.
Примечание |
---|
В этой документации для определения делегатов в PLINQ используются лямбда-выражения.Сведения о лямбда-выражениях в C# или Visual Basic см. в разделе Лямбда-выражения в PLINQ и библиотеке параллельных задач. |
Далее в статье приводится обзор основных классов PLINQ и способы создания запросов PLINQ. В каждом разделе содержатся ссылки на более подробные сведения и примеры кода.
Класс ParallelEnumerable
Класс System.Linq.ParallelEnumerable предоставляет практически все функциональные возможности PLINQ. Он и остальные типы пространства имен System.Linq компилируются в сборку System.Core.dll. Проекты C# и Visual Basic по умолчанию в Visual Studio ссылаются на эту сборку и импортируют пространство имен.
Класс ParallelEnumerable включает реализации всех стандартных операторов запросов, поддерживаемых LINQ to Objects, хотя он предпринимает попытки параллелизации каждого из них. Дополнительные сведения о LINQ см. в разделе Введение в LINQ.
Помимо стандартных операторов запросов класс ParallelEnumerable содержит набор методов, которые обеспечивают поведения, характерные для параллельного выполнения. Эти характерные для PLINQ методы перечислены в следующей таблице.
Оператор ParallelEnumerable |
Описание |
---|---|
Точка входа для PLINQ. Указывает на необходимость параллелизации остальной части запроса, если это возможно. |
|
Указывает на необходимость последовательного выполнения остальной части запроса как непараллельного запроса LINQ. |
|
Указывает, что PLINQ должен сохранить порядок исходной последовательности для остальной части запроса или до тех пор, пока порядок не будет изменен, например с помощью предложения orderby (Order By в Visual Basic). |
|
Указывает, что PLINQ не должен сохранять порядок исходной последовательности для остальной части запроса. |
|
Указывает, что PLINQ должен периодически отслеживать состояние предоставленного токена отмены и отменять выполнение при запросе. |
|
Указывает максимальное количество процессоров, которое должен использовать PLINQ для параллелизации запроса. |
|
Предоставляет подсказку о том, как PLINQ должен, если это возможно, выполнять слияние параллельных результатов в одну последовательность в потоке-потребителе. |
|
Указывает, должен ли PLINQ выполнять параллелизацию запроса, даже если согласно поведению по умолчанию он будет выполняться последовательно. |
|
Многопоточный метод перечисления, который в отличие от итерации результатов запроса позволяет обрабатывать результаты параллельно без предварительного слияния в поток-потребитель. |
|
Перегрузка Aggregate |
Уникальная для PLINQ перегрузка, обеспечивающая промежуточное агрегирование локальных частей потока, и предоставляющая функцию окончательного агрегирования для объединения результатов всех частей. |
Модель, включаемая по требованию
При написании запроса включается в PLINQ путем вызова метода расширения ParallelEnumerable.AsParallel в источнике данных, как показано в следующем примере.
Dim source = Enumerable.Range(1, 10000)
' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
Where Compute(num) > 0
Select num
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;
Метод расширения AsParallel связывает последующие операторы запросов, в этом случае where и select, с реализациями System.Linq.ParallelEnumerable.
Режимы выполнения
По умолчанию PLINQ является консервативным. Во время выполнения инфраструктура PLINQ анализирует общую структуру запроса. Если выполнение запроса скорее всего ускорится за счет параллелизации, PLINQ разделяет исходную последовательность на задачи, которые могут выполняться одновременно. Если выполнять параллелизацию запроса небезопасно, PLINQ просто выполняет запрос последовательно. Если PLINQ может выбирать между алгоритмом параллельной обработки, который потенциально требует больших затрат ресурсов, и алгоритмом последовательной обработки, не требующим больших затрат ресурсов, он выбирает алгоритм последовательной обработки по умолчанию. Чтобы указать PLINQ выбрать алгоритм параллельной обработки, можно использовать метод WithExecutionMode<TSource> и перечисление System.Linq.ParallelExecutionMode. Это полезно, если тестирование и измерение показали, что определенный запрос будет выполнять быстрее параллельно. Дополнительные сведения см. в разделе Практическое руководство. Задание режима выполнения в PLINQ.
Степень параллелизма
По умолчанию PLINQ использует все процессоры на главном компьютере до 64. Можно указать PLINQ использовать не более указанного количества процессоров с помощью метода WithDegreeOfParallelism<TSource>. Это полезно, когда требуется убедиться, что другие процессы, выполняющиеся на компьютере, получают определенное количество времени ЦП. В следующем фрагменте запрос ограничивается использованием не более двух процессоров.
Dim query = From item In source.AsParallel().WithDegreeOfParallelism(2)
Where Compute(item) > 42
Select item
var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
where Compute(item) > 42
select item;
В случаях, когда запрос выполняет значительный объем работы, не ограниченной по скорости вычислений, например файловый ввод-вывод, может быть полезно указать степень параллелизма больше, чем количество ядер в компьютере.
Упорядоченные и неупорядоченные параллельные запросы
В некоторых запросах оператор запроса должен производить результаты с сохранением порядка исходной последовательности. Для этой цели PLINQ предоставляет оператор AsOrdered. AsOrdered отличается от AsSequential<TSource>. Последовательность AsOrdered по-прежнему обрабатывается параллельно, но ее результаты буферизуются и сортируются. Поскольку сохранение порядка обычно включает дополнительную работу, последовательность AsOrdered может обрабатываться медленнее, чем последовательность AsUnordered<TSource> по умолчанию. Является ли определенная упорядоченная параллельная операция быстрее, чем последовательная, зависит от многих факторов.
В следующем примере кода показано, как использовать сохранение порядка.
Dim evenNums = From num In numbers.AsParallel().AsOrdered()
Where num Mod 2 = 0
Select num
evenNums = from num in numbers.AsParallel().AsOrdered()
where num % 2 == 0
select num;
Дополнительные сведения см. в разделе Сохранение порядка в PLINQ.
Параллельные ипоследовательные запросы
Некоторые операции требуют доставки исходных данных последовательным образом. Операторы запроса ParallelEnumerable осуществляют возврат к последовательному режиму автоматически при необходимости. Для определяемых пользователем операторов запроса и пользовательских делегатов, требующих последовательного выполнения, PLINQ предоставляет метод AsSequential<TSource>. При использовании метода AsSequential<TSource> все последующие операторы в запросе выполняются последовательно до повторного вызова метода AsParallel. Дополнительные сведения см. в разделе Практическое руководство. Объединение параллельных и последовательных запросов LINQ.
Параметры для слияния результатов запроса
При параллельном выполнении запроса PLINQ его результаты из каждого рабочего потока должны быть объединены обратно в основном потоке для использования циклом foreach (For Each в Visual Basic) либо вставки в список или массив. В некоторых случаях может быть полезно указать определенный вид операции слияния, например для начала более быстрого производства результатов. Для этого PLINQ поддерживает метод WithMergeOptions<TSource> и перечисление ParallelMergeOptions. Дополнительные сведения см. в разделе Параметры слияние в PLINQ.
Оператор ForAll
В последовательных запросах LINQ выполнение откладывается, пока запрос не будет перечислен либо в цикле foreach (For Each в Visual Basic), либо путем вызова метода, например ToList<TSource>, ToTSource> или ToDictionary. В PLINQ также можно использовать foreach для выполнения запроса и итерации результатов. Однако сам оператор foreach не выполняется параллельно, и поэтому требуется объединить выходные данные всех параллельных задач обратно в потоке, в котором выполняется цикл. В PLINQ оператор foreach можно использовать, если необходимо сохранить окончательный порядок результатов запроса, а также при каждой обработке результатов последовательным образом, например при вызове Console.WriteLine для каждого элемента. Для более быстрого выполнения запроса в случаях, когда сохранение порядка не требуется и непосредственно обработка результатов может выполняться параллельно, используйте метод ForAll<TSource> для выполнения запроса PLINQ. ForAll<TSource> не выполняет этот заключительный шаг слияния. В следующем примере кода показано, как использовать метод ForAll<TSource>. Здесь используется объект System.Collections.Concurrent.ConcurrentBag<T>, так как его оптимизация позволяет добавлять несколько потоков параллельно, не пытаясь при этом удалить какие-либо элементы.
Dim nums = Enumerable.Range(10, 10000)
Dim query = From num In nums.AsParallel()
Where num Mod 10 = 0
Select num
' Process the results as each thread completes
' and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
' which can safely accept concurrent add operations
query.ForAll(Sub(e) concurrentBag.Add(Compute(e)))
var nums = Enumerable.Range(10, 10000);
var query = from num in nums.AsParallel()
where num % 10 == 0
select num;
// Process the results as each thread completes
// and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
// which can safely accept concurrent add operations
query.ForAll((e) => concurrentBag.Add(Compute(e)));
На следующем рисунке показано различие между foreach и ForAll<TSource> по выполнению запроса.
Отмена
PLINQ интегрирован с типами отмены в .NET Framework 4. (Дополнительные сведения см. в разделе Отмена.) Таким образом, в отличие от последовательных запросов LINQ to Objects, запросы PLINQ можно отменить. Чтобы создать отменяемый запрос PLINQ, используйте оператор WithCancellation<TSource> запроса и предоставьте экземпляр CancellationToken в качестве аргумента. Если свойство IsCancellationRequested токена имеет значение true, PLINQ отметит это, остановит обработку всех потоков и создаст исключение OperationCanceledException.
Запрос PLINQ может продолжить обрабатывать некоторые элементы после задания токена отмены.
Чтобы увеличить скорость ответа, также можно отвечать на запросы отмены в длительных пользовательских делегатах. Дополнительные сведения см. в разделе Практическое руководство. Отмена запроса PLINQ.
Исключения
При выполнении запроса PLINQ может быть создано несколько исключений из разных потоков одновременно. Кроме того, код для обработки исключения может находиться не в том потоке, в котором код создал данное исключение. PLINQ использует тип AggregateException для инкапсуляции всех исключений, созданных запросом, и маршалинга этих исключений в вызывающем потоке. В вызывающем потоке необходим только один блок try-catch. Однако можно выполнить итерацию всех исключений, инкапсулированных в AggregateException, и перехватить любое исключение, из которого можно выполнить безопасное восстановление. В редких случаях могут быть созданы некоторые исключения, которые не заключены в AggregateException, и исключения ThreadAbortException также не заключаются в оболочку.
Если позволить исключениям маршрутизироваться по восходящей обратно в присоединяемый поток, запрос может продолжить обработку некоторых элементов после создания исключения.
Дополнительные сведения см. в разделе Практическое руководство. Обработка исключений в запросе PLINQ.
Пользовательские модули разделения
В некоторых случаях можно улучшить производительность, написав пользовательские модули разделения, которые используют преимущества некоторых характеристик исходных данных. В запросе сам пользовательский модуль разделения является запрашиваемым перечисляемым объектом.
[Visual Basic]
Dim arr(10000) As Integer
Dim partitioner = New MyArrayPartitioner(Of Integer)(arr)
Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))
[C#]
int[] arr= ...;
Partitioner<int> partitioner = newMyArrayPartitioner<int>(arr);
var q = partitioner.AsParallel().Select(x => SomeFunction(x));
PLINQ поддерживает фиксированное количество частей (хотя данные могут быть динамически переназначены этим частям во время выполнения для балансировки нагрузки). Методы For и ForEach поддерживают только динамическое разделение, т. е. количество частей изменяется во время выполнения. Дополнительные сведения см. в разделе Пользовательские разделители для PLINQ и TPL.
Измерение производительности PLINQ
Во многих случаях запрос можно обработать параллельно, но нагрузка при настройке параллельного запроса сводит на нет полученный выигрыш в производительности. Если в запросе не выполняется много вычислений или источник данных небольшой, запрос PLINQ может выполняться медленнее, чем последовательный запрос LINQ to Objects. Анализатор параллельной производительности в Visual Studio Team Server позволяет сравнить производительность различных запросов, чтобы обнаружить узкие места обработки и определить, выполняется ли запрос параллельно или последовательно. Дополнительные сведения см. в разделах Визуализатор параллелизма и Практическое руководство. Измерение производительности запросов PLINQ.