Введение в PLINQIntroduction to PLINQ

Что такое параллельный запрос?What is a Parallel Query?

Встроенный язык запросов (LINQ) был впервые представлен в .NET Framework 3,5.NET Framework 3.5.Language-Integrated Query (LINQ) was introduced in the .NET Framework 3,5.NET Framework 3.5. Он поддерживает унифицированную модель для запросов к любому источнику данных System.Collections.IEnumerable или System.Collections.Generic.IEnumerable<T> типобезопасным образом.It features a unified model for querying any System.Collections.IEnumerable or System.Collections.Generic.IEnumerable<T> data source in a type-safe manner. LINQ to Objects — это запросы LINQ, выполняемые с коллекциями в памяти (например, List<T>) или массивами.LINQ to Objects is the name for LINQ queries that are run against in-memory collections such as List<T> and arrays. В этой статье предполагается, что у вас уже есть общие представления о LINQ.This article assumes that you have a basic understanding of LINQ. Дополнительные сведения см. в разделе Встроенный язык запросов LINQ.For more information, see LINQ (Language-Integrated Query).

Parallel LINQ (PLINQ) является параллельной реализацией шаблона LINQ.Parallel LINQ (PLINQ) is a parallel implementation of the LINQ pattern. Запрос PLINQ во многом напоминает непараллельный запрос LINQ to Objects.A PLINQ query in many ways resembles a non-parallel LINQ to Objects query. Запросы PLINQ, как и последовательные запросы LINQLINQ, работают с любым источником данных IEnumerable или IEnumerable<T> в памяти и поддерживают отложенное выполнение, т. е. выполнение только по завершении перечисления запроса.PLINQ queries, just like sequential LINQLINQ queries, operate on any in-memory IEnumerable or IEnumerable<T> data source, and have deferred execution, which means they do not begin executing until the query is enumerated. Основное различие состоит в том, что PLINQ пытается задействовать сразу все процессоры в системе.The primary difference is that PLINQ attempts to make full use of all the processors on the system. Для этого он разбивает источник данных на сегменты, а затем запрашивается каждый сегмент в отдельном рабочем потоке сразу, используя сразу несколько процессоров.It does this by partitioning the data source into segments, and then executing the query on each segment on separate worker threads in parallel on multiple processors. Во многих случаях параллельное выполнение значительно сокращает время выполнения запроса.In many cases, parallel execution means that the query runs significantly faster.

Благодаря параллельному выполнению PLINQ позволяет существенно повысить производительность некоторых видов запросов по сравнению с устаревшим кодом. Часто для этого достаточно добавить к источнику данных оператор запроса AsParallel.Through parallel execution, PLINQ can achieve significant performance improvements over legacy code for certain kinds of queries, often just by adding the AsParallel query operation to the data source. Тем не менее параллелизм может представлять свои собственные сложности, и не все операции запросов в PLINQ выполняются быстрее.However, parallelism can introduce its own complexities, and not all query operations run faster in PLINQ. Некоторые запросы при применении параллелизма только замедляются.In fact, parallelization actually slows down certain queries. В связи с этим необходимо понимать, как влияют на параллельные запросы такие аспекты, как упорядочение.Therefore, you should understand how issues such as ordering affect parallel queries. Дополнительные сведения см. в разделе Общее представление об ускорении выполнения в PLINQ.For more information, see Understanding Speedup in PLINQ.

Примечание

В этой документации для определения делегатов в PLINQ используются лямбда-выражения.This documentation uses lambda expressions to define delegates in PLINQ. Если вы не знакомы с лямбда-выражениями в C# или Visual Basic, см. раздел Лямбда-выражения в PLINQ и TPL.If you are not familiar with lambda expressions in C# or Visual Basic, see Lambda Expressions in PLINQ and TPL.

Далее в этой статье приводится обзор основных классов PLINQ и обсуждаются способы создания запросов PLINQ.The remainder of this article gives an overview of the main PLINQ classes, and discusses how to create PLINQ queries. Каждый раздел содержит ссылки на более подробные сведения и примеры кода.Each section contains links to more detailed information and code examples.

Класс ParallelEnumerableThe ParallelEnumerable Class

Класс System.Linq.ParallelEnumerable предоставляет почти все функциональные возможности PLINQ.The System.Linq.ParallelEnumerable class exposes almost all of PLINQ's functionality. Этот класс и остальные типы пространства имен System.Linq компилируются в сборку System.Core.dll.It and the rest of the System.Linq namespace types are compiled into the System.Core.dll assembly. Проекты C# и Visual Basic по умолчанию в Visual Studio ссылаются на сборку и импортируют пространство имен.The default C# and Visual Basic projects in Visual Studio both reference the assembly and import the namespace.

ParallelEnumerable содержит реализации всех стандартных операторов запроса, поддерживаемых LINQ to Objects, но не все из них пытается выполнять параллельно.ParallelEnumerable includes implementations of all the standard query operators that LINQ to Objects supports, although it does not attempt to parallelize each one. Если вы не знакомы с LINQLINQ, обратитесь к разделу Введение в LINQ.If you are not familiar with LINQLINQ, see Introduction to LINQ.

Помимо стандартных операторов запроса, класс ParallelEnumerable содержит набор методов для реализации функций, характерных для параллельного выполнения.In addition to the standard query operators, the ParallelEnumerable class contains a set of methods that enable behaviors specific to parallel execution. Методы, характерные для PLINQ, перечислены в следующей таблице.These PLINQ-specific methods are listed in the following table.

Класс ParallelEnumerableParallelEnumerable Operator Описание:Description
AsParallel Точка входа для PLINQ.The entry point for PLINQ. Указывает, что по возможности остальная часть запроса должна быть параллелизована.Specifies that the rest of the query should be parallelized, if it is possible.
AsSequential Указывает, что остальная часть запроса должна выполняться последовательно, как непараллельный запрос LINQ.Specifies that the rest of the query should be run sequentially, as a non-parallel LINQ query.
AsOrdered Указывает, что PLINQ должен сохранить порядок исходной последовательности до конца запроса либо до тех пор, пока порядок не изменится, что может произойти, например, при использовании предложения orderby (Order By в Visual Basic).Specifies that PLINQ should preserve the ordering of the source sequence for the rest of the query, or until the ordering is changed, for example by the use of an orderby (Order By in Vlsual Basic) clause.
AsUnordered Указывает, что PLINQ для остальной части запроса не обязан сохранять порядок исходной последовательности.Specifies that PLINQ for the rest of the query is not required to preserve the ordering of the source sequence.
WithCancellation Указывает, что PLINQ должен периодически отслеживать состояние предоставленного токена отмены и отменить выполнение, если он будет запрошен.Specifies that PLINQ should periodically monitor the state of the provided cancellation token and cancel execution if it is requested.
WithDegreeOfParallelism Указывает максимальное количество процессоров, которое PLINQ должен использовать для параллелизации запроса.Specifies the maximum number of processors that PLINQ should use to parallelize the query.
WithMergeOptions Предоставляет подсказку о том, каким образом PLINQ должен объединять параллельные результаты в одну последовательность в потоке-потребителе, если это возможно.Provides a hint about how PLINQ should, if it is possible, merge parallel results back into just one sequence on the consuming thread.
WithExecutionMode Указывает, должен ли PLINQ параллелизовать запрос, даже если по умолчанию он должен выполняться последовательно.Specifies whether PLINQ should parallelize the query even when the default behavior would be to run it sequentially.
ForAll Многопоточный метод перечисления в отличие от итерации результатов запроса может обрабатываться параллельно без предварительного объединения с потоком-потребителем.A multithreaded enumeration method that, unlike iterating over the results of the query, enables results to be processed in parallel without first merging back to the consumer thread.
Перегрузка AggregateAggregate overload Перегрузка, которая является уникальной для PLINQ и обеспечивает промежуточное агрегирование локальных разделов потока, а также функцию окончательного агрегирования, позволяющую объединять результаты всех разделов.An overload that is unique to PLINQ and enables intermediate aggregation over thread-local partitions, plus a final aggregation function to combine the results of all partitions.

Модель с явным согласиемThe Opt-in Model

Когда вы создаете запрос, подтвердите согласие на использование PLINQ вызовом метода расширения ParallelEnumerable.AsParallel для источника данных, как показано в следующем примере.When you write a query, opt in to PLINQ by invoking the ParallelEnumerable.AsParallel extension method on the data source, as shown in the following example.

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.The AsParallel extension method binds the subsequent query operators, in this case, where and select, to the System.Linq.ParallelEnumerable implementations.

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

По умолчанию PLINQ является консервативным.By default, PLINQ is conservative. Во время выполнения инфраструктура PLINQ анализирует общую структуру запроса.At run time, the PLINQ infrastructure analyzes the overall structure of the query. Если параллелизация может ускорить выполнение запроса, PLINQ разбивает исходную последовательность на задачи, которые выполняются одновременно.If the query is likely to yield speedups by parallelization, PLINQ partitions the source sequence into tasks that can be run concurrently. Если параллелизовать запрос небезопасно, PLINQ просто выполняет его последовательно.If it is not safe to parallelize a query, PLINQ just runs the query sequentially. Если PLINQ может выбирать между потенциально затратным параллельным алгоритмом или нетребовательным последовательным алгоритмом, по умолчанию он выбирает алгоритм последовательной обработки.If PLINQ has a choice between a potentially expensive parallel algorithm or an inexpensive sequential algorithm, it chooses the sequential algorithm by default. Метод WithExecutionMode и перечисление System.Linq.ParallelExecutionMode позволяют указать, что PLINQ следует выбрать параллельный алгоритм.You can use the WithExecutionMode method and the System.Linq.ParallelExecutionMode enumeration to instruct PLINQ to select the parallel algorithm. Это пригодится в том случае, если тестирование и измерение показали, что в параллельном режиме определенный запрос будет выполнять быстрее.This is useful when you know by testing and measurement that a particular query executes faster in parallel. Дополнительные сведения см. в разделе Практическое руководство. Указание режима выполнения в PLINQ.For more information, see How to: Specify the Execution Mode in PLINQ.

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

По умолчанию PLINQ использует все процессоры на главном компьютере.By default, PLINQ uses all of the processors on the host computer. Вы можете ограничить число процессоров, используемых PLINQ, с помощью метода WithDegreeOfParallelism.You can instruct PLINQ to use no more than a specified number of processors by using the WithDegreeOfParallelism method. Это пригодится в том случае, если вам нужно, чтобы другие процессы, выполняемые на том же компьютере, получали определенное количество процессорного времени.This is useful when you want to make sure that other processes running on the computer receive a certain amount of CPU time. Следующий фрагмент кода позволяет запросу использовать не более двух процессоров.The following snippet limits the query to utilizing a maximum of two processors.

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

В случаях, когда запрос выполняет значительный объем работы, не связанной с вычислениями, такой как ввод-вывод файлов, степень параллелизма может быть больше количества ядер на соответствующем компьютере.In cases where a query is performing a significant amount of non-compute-bound work such as File I/O, it might be beneficial to specify a degree of parallelism greater than the number of cores on the machine.

Упорядоченные и неупорядоченные параллельные запросыOrdered Versus Unordered Parallel Queries

В некоторых случаях оператор запроса должен выдавать результаты с сохранением порядка исходной последовательности.In some queries, a query operator must produce results that preserve the ordering of the source sequence. Для этого PLINQ предоставляет оператор AsOrdered.PLINQ provides the AsOrdered operator for this purpose. AsOrdered отличается от AsSequential.AsOrdered is distinct from AsSequential. Последовательность AsOrdered по-прежнему обрабатывается параллельно, но ее результаты помещаются в буфер и сортируются.An AsOrdered sequence is still processed in parallel, but its results are buffered and sorted. Поскольку сохранение порядка обычно требует дополнительной работы, последовательность AsOrdered может обрабатываться медленнее, чем стандартная последовательность AsUnordered.Because order preservation typically involves extra work, an AsOrdered sequence might be processed more slowly than the default AsUnordered sequence. Будет ли та или иная упорядоченная параллельная операция выполняться быстрее, чем ее последовательная версия, зависит от многих факторов.Whether a particular ordered parallel operation is faster than a sequential version of the operation depends on many factors.

В следующем примере кода показано, как разрешить сохранение порядка.The following code example shows how to opt in to order preservation.

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.For more information, see Order Preservation in PLINQ.

Сравнение параллельных и последовательных запросовParallel vs. Sequential Queries

Некоторые операции требуют, чтобы исходные данные доставлялись последовательно.Some operations require that the source data be delivered in a sequential manner. При необходимости операторы запроса ParallelEnumerable автоматически переходят в последовательный режим.The ParallelEnumerable query operators revert to sequential mode automatically when it is required. Для пользовательских операторов запроса и делегатов, которые требуют последовательного выполнения, PLINQ предоставляет метод AsSequential.For user-defined query operators and user delegates that require sequential execution, PLINQ provides the AsSequential method. При использовании метода AsSequential все операторы, содержащиеся в запросе, будут выполняться последовательно вплоть до следующего вызова AsParallel.When you use AsSequential, all subsequent operators in the query are executed sequentially until AsParallel is called again. Дополнительные сведения см. в разделе Практическое руководство. Объединение параллельных и последовательных запросов LINQ.For more information, see How to: Combine Parallel and Sequential LINQ Queries.

Параметры для слияния результатов запросаOptions for Merging Query Results

При параллельном выполнении запроса PLINQ его результаты из каждого рабочего потока должны быть снова объединены с основным потоком для использования циклом foreach (For Each в Visual Basic) либо вставки в список или массив.When a PLINQ query executes in parallel, its results from each worker thread must be merged back onto the main thread for consumption by a foreach loop (For Each in Visual Basic), or insertion into a list or array. В некоторых случаях может быть полезно указать конкретный вид операции слияния, например для того, чтобы получать результаты быстрее.In some cases, it might be beneficial to specify a particular kind of merge operation, for example, to begin producing results more quickly. Для этого PLINQ поддерживает метод WithMergeOptions и перечисление ParallelMergeOptions.For this purpose, PLINQ supports the WithMergeOptions method, and the ParallelMergeOptions enumeration. Дополнительные сведения см. в разделе Параметры слияние в PLINQ.For more information, see Merge Options in PLINQ.

Оператор ForAllThe ForAll Operator

В последовательных запросах LINQLINQ выполнение откладывается до того момента, когда завершится перечисление запроса в цикле foreach (For Each в Visual Basic) или будет вызван метод ToList, ToArray или ToDictionary.In sequential LINQLINQ queries, execution is deferred until the query is enumerated either in a foreach (For Each in Visual Basic) loop or by invoking a method such as ToList , ToArray , or ToDictionary. Кроме того, для выполнения запроса и итерации результатов в PLINQ можно использовать foreach.In PLINQ, you can also use foreach to execute the query and iterate through the results. При этом сам оператор foreach параллельно не выполняется, а значит результаты всех параллельных задач необходимо снова объединить с тем потоком, в котором выполняется цикл.However, foreach itself does not run in parallel, and therefore, it requires that the output from all parallel tasks be merged back into the thread on which the loop is running. Оператор foreach можно использовать в PLINQ, если вам нужно сохранить окончательный порядок результатов запроса, а также при любой последовательной обработке результатов (например, при вызове Console.WriteLine для каждого элемента).In PLINQ, you can use foreach when you must preserve the final ordering of the query results, and also whenever you are processing the results in a serial manner, for example when you are calling Console.WriteLine for each element. Чтобы ускорить выполнение запроса в ситуации, когда сохранение порядка не требуется и обработка результатов допускает параллелизацию, используйте для выполнения запроса PLINQ метод ForAll.For faster query execution when order preservation is not required and when the processing of the results can itself be parallelized, use the ForAll method to execute a PLINQ query. ForAll не выполняет этот заключительный шаг слияния.ForAll does not perform this final merge step. В следующем примере кода показано применение метода ForAll.The following code example shows how to use the ForAll method. System.Collections.Concurrent.ConcurrentBag<T> используется здесь потому, что он оптимизирован для одновременного добавления данных из нескольких потоков и не пытается удалять элементы.System.Collections.Concurrent.ConcurrentBag<T> is used here because it is optimized for multiple threads adding concurrently without attempting to remove any items.

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 в выполнении запросов.The following illustration shows the difference between foreach and ForAll with regard to query execution.

Сравнение ForAll и ForEachForAll vs. ForEach

ОтменаCancellation

PLINQ интегрирован с типами отмены в .NET Framework 4.NET Framework 4.PLINQ is integrated with the cancellation types in .NET Framework 4.NET Framework 4. (Дополнительные сведения см. в разделе Отмена в управляемых потоках.) Это значит, что в отличие от последовательных запросов LINQ to Objects запросы PLINQ можно отменять.(For more information, see Cancellation in Managed Threads.) Therefore, unlike sequential LINQ to Objects queries, PLINQ queries can be canceled. Чтобы создать запрос PLINQ с возможностью отмены, примените в запросе оператор WithCancellation и предоставьте ему экземпляр CancellationToken в качестве аргумента.To create a cancelable PLINQ query, use the WithCancellation operator on the query and provide a CancellationToken instance as the argument. Когда свойство IsCancellationRequested для маркера примет значение TRUE, PLINQ заметит это и остановит обработку всех потоков, а затем создаст исключение OperationCanceledException.When the IsCancellationRequested property on the token is set to true, PLINQ will notice it, stop processing on all threads, and throw an OperationCanceledException.

Существует вероятность, что запрос PLINQ продолжит обработку некоторых элементов после того, как будет задан маркер отмены.It is possible that a PLINQ query might continue to process some elements after the cancellation token is set.

Для повышения скорости реагирования можно также отвечать на запросы отмены в пользовательских делегатах, выполняемых долгое время.For greater responsiveness, you can also respond to cancellation requests in long-running user delegates. Дополнительные сведения см. в разделе Практическое руководство. Отмена запроса PLINQ.For more information, see How to: Cancel a PLINQ Query.

ИсключенияExceptions

При выполнении запроса PLINQ могут быть одновременно выданы сразу несколько исключений из разных потоков.When a PLINQ query executes, multiple exceptions might be thrown from different threads simultaneously. Кроме того, код для обработки исключения может находиться не в том потоке, где находится код, который вызвал исключение.Also, the code to handle the exception might be on a different thread than the code that threw the exception. С помощью типа AggregateException PLINQ инкапсулирует все исключения, созданные запросом, и маршалирует эти исключения в вызывающий поток.PLINQ uses the AggregateException type to encapsulate all the exceptions that were thrown by a query, and marshal those exceptions back to the calling thread. В вызывающем потоке должен присутствовать только один блок try-catch.On the calling thread, only one try-catch block is required. Но в нем вы можете последовательно просмотреть все инкапсулированные в AggregateExceptionисключения и обработать те из них, которые допускают безопасное восстановление.However, you can iterate through all of the exceptions that are encapsulated in the AggregateException and catch any that you can safely recover from. В редких случаях могут создаваться исключения, не упакованные в AggregateException. Также этот механизм не применяется для ThreadAbortException.In rare cases, some exceptions may be thrown that are not wrapped in an AggregateException, and ThreadAbortExceptions are also not wrapped.

Если исключения могут всплывать обратно в присоединяемый поток, запрос может продолжить обработку некоторых элементов после создания исключения.When exceptions are allowed to bubble up back to the joining thread, then it is possible that a query may continue to process some items after the exception is raised.

Дополнительные сведения см. в разделе Практическое руководство. Обработка исключений в запросе PLINQ.For more information, see How to: Handle Exceptions in a PLINQ Query.

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

В некоторых случаях производительность запросов можно улучшить, написав пользовательский модуль разделения, который использует преимущества некоторых характеристик исходных данных.In some cases, you can improve query performance by writing a custom partitioner that takes advantage of some characteristic of the source data. В запросе сам пользовательский модуль разделения является запрашиваемым перечислимым объектом.In the query, the custom partitioner itself is the enumerable object that is queried.

int[] arr = new int[9999];
Partitioner<int> partitioner = new MyArrayPartitioner<int>(arr);
var query = partitioner.AsParallel().Select(x => SomeFunction(x));
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 поддерживает фиксированное количество разделов (хотя для балансировки нагрузки данные могут динамически переназначаться этим разделам во время выполнения).PLINQ supports a fixed number of partitions (although data may be dynamically reassigned to those partitions during run time for load balancing.). For и ForEach поддерживают только динамическое секционирование, а значит количество секций может изменяться во время выполнения.For and ForEach support only dynamic partitioning, which means that the number of partitions changes at run time. Дополнительные сведения см. в разделе Пользовательские разделители для PLINQ и TPL.For more information, see Custom Partitioners for PLINQ and TPL.

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

Во многих случаях запрос может выполняться параллельно, но на настройку параллельного запроса уходит больше времени, чем будет выиграно в результате.In many cases, a query can be parallelized, but the overhead of setting up the parallel query outweighs the performance benefit gained. Если запрос не выполняет большой объем вычислений или источник данных небольшой, запрос PLINQ может быть медленнее, чем последовательный запрос LINQ to Objects.If a query does not perform much computation or if the data source is small, a PLINQ query may be slower than a sequential LINQ to Objects query. Анализатор параллельной производительности в Visual Studio Team Server позволяет сравнивать производительность различных запросов и таким образом выявлять проблемы обработки и определять, выполняется ли запрос параллельно или последовательно.You can use the Parallel Performance Analyzer in Visual Studio Team Server to compare the performance of various queries, to locate processing bottlenecks, and to determine whether your query is running in parallel or sequentially. Дополнительные сведения см. в разделах Визуализатор параллелизма и Практическое руководство. Измерение производительности запросов PLINQ.For more information, see Concurrency Visualizer and How to: Measure PLINQ Query Performance.

См. такжеSee also