Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Parallel LINQ (PLINQ) — это параллельная реализация шаблона Language-Integrated запроса (LINQ). PLINQ реализует полный набор стандартных операторов запросов LINQ в качестве методов расширения для System.Linq пространства имен и имеет дополнительные операторы для параллельных операций. PLINQ объединяет простоту и удобочитаемость синтаксиса LINQ с возможностью параллельного программирования.
Подсказка
Если вы не знакомы с LINQ, она имеет единую модель для запроса любого перечисленного источника данных в типобезопасном режиме. LINQ to Objects — это название для запросов LINQ, которые выполняются в коллекциях, находящихся в памяти, таких как List<T> и массивы. В этой статье предполагается, что у вас есть базовое понимание LINQ. Дополнительные сведения см. в разделеLanguage-Integrated Query (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#) и введение в LINQ (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($"{evenNums.Count()} even numbers out of {source.Count()} total");
// 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
сама по себе не выполняется параллельно, поэтому требуется, чтобы выходные данные всех параллельных задач были объединены обратно в поток, на котором выполняется цикл. В PLINQ можно использовать foreach
, если необходимо сохранить окончательное упорядочение результатов запроса, а также при обработке результатов последовательно, например при вызове Console.WriteLine
каждого элемента. Чтобы ускорить выполнение запросов, если сохранение порядка не требуется, и когда обработка результатов может быть параллелизирована, используйте ForAll метод для выполнения запроса PLINQ.
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 в отношении выполнения запросов.
Отмена
PLINQ интегрирован с типами отмены в .NET. (Дополнительные сведения см. в разделе "Отмена" в управляемых потоках.) Таким образом, в отличие от последовательных запросов LINQ to Objects, запросы PLINQ можно отменить. Чтобы создать отменяемый запрос PLINQ, используйте WithCancellation оператор в запросе и укажите CancellationToken экземпляр в качестве аргумента. Если свойству IsCancellationRequested для маркера присвоено значение true, PLINQ заметит это, остановит обработку на всех потоках и вызовет OperationCanceledException исключение.
Возможно, запрос PLINQ может продолжать обрабатывать некоторые элементы после установки маркера отмены.
Для повышения скорости реагирования можно также реагировать на запросы отмены в длительных делегатах пользователей. Дополнительные сведения см. в разделе "Практическое руководство. Отмена запроса PLINQ".
Исключения
При выполнении запроса PLINQ несколько исключений могут вызываться одновременно из разных потоков. Кроме того, код для обработки исключения может находиться в другом потоке, отличном от кода, вызвавшего исключение. PLINQ использует тип AggregateException, чтобы инкапсулировать все исключения, которые были вызваны при выполнении запроса, и передать эти исключения обратно в вызывающий поток. В вызывающем потоке требуется только один блок 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.