Введение в PLINQ

Parallel LINQ (PLINQ) является параллельной реализацией шаблона LINQ (Language-Integrated Query). PLINQ реализует полный набор стандартных операторов запроса LINQ как методов расширения для пространства имен System.Linq и имеет дополнительные операторы для параллельных операций. PLINQ объединяет простоту и удобство чтения синтаксиса LINQ с мощностью параллельного программирования.

Совет

Если вы не знакомы с LINQ, он поддерживает унифицированную модель для запросов к любому перечислимому источнику данных типобезопасным образом. LINQ to Objects — это запросы LINQ, выполняемые с коллекциями в памяти (например, List<T>) или массивами. В этой статье предполагается, что у вас уже есть общие представления о LINQ. Дополнительные сведения см. в статье Синтаксис LINQ.

Что такое параллельный запрос?

Запрос PLINQ во многом напоминает непараллельный запрос LINQ to Objects. Запросы PLINQ, как и последовательные запросы LINQ, работают с любым источником данных IEnumerable или IEnumerable<T> в памяти и поддерживают отложенное выполнение, т. е. выполнение только по завершении перечисления запроса. Основное различие состоит в том, что PLINQ пытается задействовать сразу все процессоры в системе. Для этого он разбивает источник данных на сегменты, а затем запрашивается каждый сегмент в отдельном рабочем потоке сразу, используя сразу несколько процессоров. Во многих случаях параллельное выполнение значительно сокращает время выполнения запроса.

Благодаря параллельному выполнению PLINQ позволяет существенно повысить производительность некоторых видов запросов по сравнению с устаревшим кодом. Часто для этого достаточно добавить к источнику данных оператор запроса AsParallel. Тем не менее параллелизм может представлять свои собственные сложности, и не все операции запросов в PLINQ выполняются быстрее. Некоторые запросы при применении параллелизма только замедляются. В связи с этим необходимо понимать, как влияют на параллельные запросы такие аспекты, как упорядочение. Дополнительные сведения см. в разделе Общее представление об ускорении выполнения в PLINQ.

Примечание

В этой документации для определения делегатов в PLINQ используются лямбда-выражения. Если вы не знакомы с лямбда-выражениями в C# или Visual Basic, см. раздел Лямбда-выражения в PLINQ и TPL.

Далее в этой статье приводится обзор основных классов PLINQ и обсуждаются способы создания запросов PLINQ. Каждый раздел содержит ссылки на более подробные сведения и примеры кода.

Класс ParallelEnumerable

Класс System.Linq.ParallelEnumerable предоставляет почти все функциональные возможности PLINQ. Этот класс и остальные типы пространства имен System.Linq компилируются в сборку System.Core.dll. Проекты C# и Visual Basic по умолчанию в Visual Studio ссылаются на сборку и импортируют пространство имен.

ParallelEnumerable содержит реализации всех стандартных операторов запроса, поддерживаемых LINQ to Objects, но не все из них пытается выполнять параллельно. Если вы не использовали LINQ, см. общие сведения о LINQ для C# и Visual Basic.

Помимо стандартных операторов запроса, класс ParallelEnumerable содержит набор методов для реализации функций, характерных для параллельного выполнения. Методы, характерные для PLINQ, перечислены в следующей таблице.

Класс ParallelEnumerable Описание
AsParallel Точка входа для PLINQ. Указывает, что по возможности остальная часть запроса должна быть параллелизована.
AsSequential Указывает, что остальная часть запроса должна выполняться последовательно, как непараллельный запрос LINQ.
AsOrdered Указывает, что PLINQ должен сохранить порядок исходной последовательности до конца запроса либо до тех пор, пока порядок не изменится, что может произойти, например, при использовании предложения orderby (Order By в Visual Basic).
AsUnordered Указывает, что PLINQ для остальной части запроса не обязан сохранять порядок исходной последовательности.
WithCancellation Указывает, что PLINQ должен периодически отслеживать состояние предоставленного токена отмены и отменить выполнение, если он будет запрошен.
WithDegreeOfParallelism Указывает максимальное количество процессоров, которое PLINQ должен использовать для параллелизации запроса.
WithMergeOptions Предоставляет подсказку о том, каким образом PLINQ должен объединять параллельные результаты в одну последовательность в потоке-потребителе, если это возможно.
WithExecutionMode Указывает, должен ли PLINQ параллелизовать запрос, даже если по умолчанию он должен выполняться последовательно.
ForAll Многопоточный метод перечисления в отличие от итерации результатов запроса может обрабатываться параллельно без предварительного объединения с потоком-потребителем.
Перегрузка Aggregate Перегрузка, которая является уникальной для PLINQ и обеспечивает промежуточное агрегирование локальных разделов потока, а также функцию окончательного агрегирования, позволяющую объединять результаты всех разделов.

Модель с явным согласием

Когда вы создаете запрос, подтвердите согласие на использование PLINQ вызовом метода расширения ParallelEnumerable.AsParallel для источника данных, как показано в следующем примере.

var source = Enumerable.Range(1, 10000);

// Opt in to PLINQ with AsParallel.
var evenNums = from num in source.AsParallel()
               where num % 2 == 0
               select num;
Console.WriteLine("{0} even numbers out of {1} total",
                  evenNums.Count(), source.Count());
// The example displays the following output:
//       5000 even numbers out of 10000 total
Dim source = Enumerable.Range(1, 10000)

' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
               Where num Mod 2 = 0
               Select num
Console.WriteLine("{0} even numbers out of {1} total",
                  evenNums.Count(), source.Count())
' The example displays the following output:
'       5000 even numbers out of 10000 total

Метод расширения AsParallel привязывает последующие операторы запросов (в нашем примере это where и select) к реализациям System.Linq.ParallelEnumerable.

Режимы выполнения

По умолчанию PLINQ является консервативным. Во время выполнения инфраструктура PLINQ анализирует общую структуру запроса. Если параллелизация может ускорить выполнение запроса, PLINQ разбивает исходную последовательность на задачи, которые выполняются одновременно. Если параллелизовать запрос небезопасно, PLINQ просто выполняет его последовательно. Если PLINQ может выбирать между потенциально затратным параллельным алгоритмом или нетребовательным последовательным алгоритмом, по умолчанию он выбирает алгоритм последовательной обработки. Метод WithExecutionMode и перечисление System.Linq.ParallelExecutionMode позволяют указать, что PLINQ следует выбрать параллельный алгоритм. Это пригодится в том случае, если тестирование и измерение показали, что в параллельном режиме определенный запрос будет выполнять быстрее. Дополнительные сведения см. в разделе Практическое руководство. Задание режима выполнения в PLINQ.

Степень параллелизма

По умолчанию PLINQ использует все процессоры на главном компьютере. Вы можете ограничить число процессоров, используемых PLINQ, с помощью метода WithDegreeOfParallelism. Это пригодится в том случае, если вам нужно, чтобы другие процессы, выполняемые на том же компьютере, получали определенное количество процессорного времени. Следующий фрагмент кода позволяет запросу использовать не более двух процессоров.

var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
            where Compute(item) > 42
            select item;
Dim query = From item In source.AsParallel().WithDegreeOfParallelism(2)
            Where Compute(item) > 42
            Select item

В случаях, когда запрос выполняет значительный объем работы, не связанной с вычислениями, такой как ввод-вывод файлов, степень параллелизма может быть больше количества ядер на соответствующем компьютере.

Упорядоченные и неупорядоченные параллельные запросы

В некоторых случаях оператор запроса должен выдавать результаты с сохранением порядка исходной последовательности. Для этого PLINQ предоставляет оператор AsOrdered. AsOrdered отличается от AsSequential. Последовательность AsOrdered по-прежнему обрабатывается параллельно, но ее результаты помещаются в буфер и сортируются. Поскольку сохранение порядка обычно требует дополнительной работы, последовательность AsOrdered может обрабатываться медленнее, чем стандартная последовательность AsUnordered. Будет ли та или иная упорядоченная параллельная операция выполняться быстрее, чем ее последовательная версия, зависит от многих факторов.

В следующем примере кода показано, как разрешить сохранение порядка.

var evenNums =
    from num in numbers.AsParallel().AsOrdered()
    where num % 2 == 0
    select num;
Dim evenNums = From num In numbers.AsParallel().AsOrdered()
               Where num Mod 2 = 0
               Select num


Дополнительные сведения см. в разделе Сохранение порядка в PLINQ.

Сравнение параллельных и последовательных запросов

Некоторые операции требуют, чтобы исходные данные доставлялись последовательно. При необходимости операторы запроса ParallelEnumerable автоматически переходят в последовательный режим. Для пользовательских операторов запроса и делегатов, которые требуют последовательного выполнения, PLINQ предоставляет метод AsSequential. При использовании метода AsSequential все операторы, содержащиеся в запросе, будут выполняться последовательно вплоть до следующего вызова AsParallel. Дополнительные сведения см. в разделе Практическое руководство. Объединение параллельных и последовательных запросов LINQ.

Параметры для слияния результатов запроса

При параллельном выполнении запроса PLINQ его результаты из каждого рабочего потока должны быть снова объединены с основным потоком для использования циклом foreach (For Each в Visual Basic) либо вставки в список или массив. В некоторых случаях может быть полезно указать конкретный вид операции слияния, например для того, чтобы получать результаты быстрее. Для этого PLINQ поддерживает метод WithMergeOptions и перечисление ParallelMergeOptions. Дополнительные сведения см. в разделе Параметры слияние в PLINQ.

Оператор ForAll

В последовательных запросах LINQ выполнение откладывается до того момента, когда завершится перечисление запроса в цикле foreach (For Each в Visual Basic) или будет вызван метод ToList, ToArray или ToDictionary. Кроме того, для выполнения запроса и итерации результатов в PLINQ можно использовать foreach. При этом сам оператор foreach параллельно не выполняется, а значит результаты всех параллельных задач необходимо снова объединить с тем потоком, в котором выполняется цикл. Оператор foreach можно использовать в PLINQ, если вам нужно сохранить окончательный порядок результатов запроса, а также при любой последовательной обработке результатов (например, при вызове Console.WriteLine для каждого элемента). Чтобы ускорить выполнение запроса в ситуации, когда сохранение порядка не требуется и обработка результатов допускает параллелизацию, используйте для выполнения запроса PLINQ метод ForAll. ForAll не выполняет этот заключительный шаг слияния. В следующем примере кода показано применение метода ForAll. System.Collections.Concurrent.ConcurrentBag<T> используется здесь потому, что он оптимизирован для одновременного добавления данных из нескольких потоков и не пытается удалять элементы.

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

Ниже демонстрируется разница между foreach и ForAll в выполнении запросов.

Сравнение ForAll и ForEach

Отмена

PLINQ интегрирован с типами отмены в .NET. (Дополнительные сведения см. в разделе Отмена в управляемых потоках.) Это значит, что в отличие от последовательных запросов LINQ to Objects запросы PLINQ можно отменять. Чтобы создать запрос PLINQ с возможностью отмены, примените в запросе оператор WithCancellation и предоставьте ему экземпляр CancellationToken в качестве аргумента. Когда свойство IsCancellationRequested для маркера примет значение TRUE, PLINQ заметит это и остановит обработку всех потоков, а затем создаст исключение OperationCanceledException.

Существует вероятность, что запрос PLINQ продолжит обработку некоторых элементов после того, как будет задан маркер отмены.

Для повышения скорости реагирования можно также отвечать на запросы отмены в пользовательских делегатах, выполняемых долгое время. Дополнительные сведения см. в разделе Практическое руководство. Отмена запроса PLINQ.

Исключения

При выполнении запроса PLINQ могут быть одновременно выданы сразу несколько исключений из разных потоков. Кроме того, код для обработки исключения может находиться не в том потоке, где находится код, который вызвал исключение. С помощью типа AggregateException PLINQ инкапсулирует все исключения, созданные запросом, и маршалирует эти исключения в вызывающий поток. В вызывающем потоке должен присутствовать только один блок try-catch. Но в нем вы можете последовательно просмотреть все инкапсулированные в AggregateExceptionисключения и обработать те из них, которые допускают безопасное восстановление. В редких случаях могут создаваться исключения, не упакованные в AggregateException. Также этот механизм не применяется для ThreadAbortException.

Если исключения могут всплывать обратно в присоединяемый поток, запрос может продолжить обработку некоторых элементов после создания исключения.

Дополнительные сведения см. в разделе Практическое руководство. Обработка исключений в запросе PLINQ.

Пользовательские разделители

В некоторых случаях производительность запросов можно улучшить, написав пользовательский модуль разделения, который использует преимущества некоторых характеристик исходных данных. В запросе сам пользовательский модуль разделения является запрашиваемым перечислимым объектом.

int[] arr = new int[9999];
Partitioner<int> partitioner = new MyArrayPartitioner<int>(arr);
var query = partitioner.AsParallel().Select(SomeFunction);
Dim arr(10000) As Integer
Dim partitioner As Partitioner(Of Integer) = New MyArrayPartitioner(Of Integer)(arr)
Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))

PLINQ поддерживает фиксированное количество разделов (хотя для балансировки нагрузки данные могут динамически переназначаться этим разделам во время выполнения). For и ForEach поддерживают только динамическое секционирование, а значит количество секций может изменяться во время выполнения. Дополнительные сведения см. в разделе Пользовательские разделители для PLINQ и TPL.

Измерение производительности PLINQ

Во многих случаях запрос может выполняться параллельно, но на настройку параллельного запроса уходит больше времени, чем будет выиграно в результате. Если запрос не выполняет большой объем вычислений или источник данных небольшой, запрос PLINQ может быть медленнее, чем последовательный запрос LINQ to Objects. Анализатор параллельной производительности в Visual Studio Team Server позволяет сравнивать производительность различных запросов и таким образом выявлять проблемы обработки и определять, выполняется ли запрос параллельно или последовательно. Дополнительные сведения см. в статье Визуализатор параллелизма и Практическое руководство. Измерение производительности запросов PLINQ.

См. также