Урок 15. Параллельный LINQ


Пятнадцатый и заключительный урок учебника по параллельному программированию в .NET заканчивает изучение императивного программирования с использованием циклов и задач. На нем мы начнем описание декларативного программирования с использованием параллельного LINQ.

Язык интегрированных запросов

Language-Integrated Query (LINQ) предоставляет декларативную модель, которая позволяет запрашивать последовательности данных, таких как коллекции в памяти, XML-документы и данные базы данных. В отличие от императивного кода, который мы видели до сих пор в этом учебнике, когда вы используете LINQ, вас интересует то, чего вы пытаетесь достичь, а не механика того, как вы этого достигаете; LINQ скрывает детали реализации циклов и условных операторов, позволяя выполнять запросы с помощью лямбда-выражений, стандартных операторов запросов и нового синтаксиса запросов.

Природа многих запросов означает, что их можно легко распараллелить. Большинство запросов выполняют одну и ту же группу действий для каждого элемента в коллекции. Если все эти действия независимы, без каких-либо побочных эффектов, вызванных порядком их появления, вы часто можете добиться значительного увеличения производительности, разделив работу между несколькими процессорными ядрами. Для поддержки этих случаях .NET framework версии 4.0 представила параллельный LINQ (PLINQ).

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

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

Второе ограничение заключается в том, что PLINQ обеспечивает параллелизм только для данных в памяти, таких как коллекции или предварительно загруженный XML. Другие источники данных будут обрабатываться последовательно. Например, LINQ to SQL генерирует операторы SQL, которые передаются SQL Server и возвращаются результаты. Однако дальнейшая обработка этих возвращенных результатов может быть распараллелена с помощью PLINQ.

AsParallel

Чтобы показать, как запрос LINQ может быть изменен для параллельной обработки, нам сначала нужна последовательная версия. Приведенный ниже код показывает очень простой запрос. Здесь мы начнем с массива, содержащего целые числа от одного до десяти. Используя стандартный оператор запроса Select, мы проецируем его на новую последовательность, содержащую квадраты исходных значений. Поскольку LINQ использует отложенное выполнение, новая последовательность не генерируется до тех пор, пока не будет получен доступ к данным. Это означает, что цикл foreach вызывает вычисление запроса и выводит результаты.

int[] sequence = Enumerable.Range(1, 10).ToArray();
 
var squares = sequence.Select(x => x * x);
 
foreach (var square in squares)
{
    Console.Write(square + " ");
}
 
/* OUTPUT
 
1 4 9 16 25 36 49 64 81 100
 
*/
LINQ работает с последовательностями, реализующими интерфейс IEnumerable. Чтобы показать, что мы хотим использовать PLINQ, мы должны убедиться, что исходная последовательность поддерживает параллелизм. Для этого мы можем использовать статический метод AsParallel класса ParallelEnumerable. Это метод расширения IEnumerable<T>, поэтому он может быть применен к любой последовательности, поддерживающей операции LINQ. Он возвращает объект типа ParallelQuery.

Как только у вас есть параллельная последовательность данных, вы можете использовать ее в качестве источника для операций LINQ, как и любую другую последовательность. Выполнение запросов по-прежнему откладывается, и отдельные результаты остаются теми же. Однако в фоновом режиме PLINQ декомпозирует данные таким образом, чтобы обеспечить эффективную параллельную обработку.

Чтобы распараллелить первичный запрос, добавьте вызов AsParallel, как показано ниже:

var squares = sequence.AsParallel().Select(x => x * x);

// 1 36 4 49 9 64 16 81 25 100 
Сохранение порядка

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

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

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

Следующий запрос использует метод метод asordered после asparallel, чтобы сохранить порядок следования результатов.

var squares = sequence.AsParallel().AsOrdered().Select(x => x * x);
 
// 1 4 9 16 25 36 49 64 81 100
Использование AsParallel с синтаксисом Query Expression

Давайте рассмотрим распараллеливание запроса, определенного с помощью синтаксиса выражения запроса. Во-первых, нам нужен подходящий запрос. Мы снова сгенерируем квадраты исходной последовательности. Для этого замените предыдущую строку запроса следующей:

var squares = 
    from x in sequence
    select x * x;
 
// 1 4 9 16 25 36 49 64 81 100
Опять же, чтобы выполнить запрос параллельно, мы просто применяем метод AsParallel к исходным данным, как показано ниже:

var squares = 
    from x in sequence.AsParallel()
    select x * x;
 
// 1 36 4 49 9 64 16 81 25 100 
Как и прежде, мы можем сохранить порядок результатов с помощью метода AsOrdered:

var squares = 
    from x in sequence.AsParallel().AsOrdered()
    select x * x;
 
// 1 4 9 16 25 36 49 64 81 100
Обработка исключений с помощью PLINQ

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

Чтобы иметь дело с возможностью запроса, вызывающего несколько исключений, все исключения из запроса PLINQ объединяются в одно исключение AggregateException, которое генерируется, когда все потоки выполнения останавливаются. Как и при использовании параллельных циклов и задач, вы можете захватить это и изучить его свойство InnerExceptions, чтобы найти все брошенные исключения.

Чтобы продемонстрировать это, попробуйте выполнить следующий пример кода несколько раз. Этот код выполняет ряд операций деления. Два значения в последовательности равны нулю и вызовут исключение DivideByZeroException. При запуске кода Вы увидите, что иногда возникает одно исключение, а иногда два, в зависимости от того, встречается ли второй ноль до того, как первое исключение остановит весь запрос.

int[] sequence = new int[] { 1, 2, 3, 4, 0, 6, 12, 24, 48, 0 };
 
var results =
    from x in sequence.AsParallel()
    select 96 / x;
 
try
{
    foreach (var result in results)
    {
        Console.Write(result + " ");
    }
}
catch (AggregateException ex)
{
    foreach (var exception in ex.InnerExceptions)
    {
        Console.WriteLine(exception.Message);
    }
}
 
/* OUTPUT
 
Attempted to divide by zero.
Attempted to divide by zero.
 
*/
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегистатьи IT, параллельное программирование, си шарп, LINQ




Отправляя сообщение я подтверждаю, что ознакомлен и согласен с политикой конфиденциальности данного сайта.



Урок 13. Знакомство с хелпером, группами маршрутов и посредниками Laravel
Обзор Java Date и Time API
Перенос базы статей c ModX на WP