Технология PLINQ
Порядок элементов
При выполнении параллельных запросов не гарантируется сохранение порядка элементов в результирующей последовательности.
Для гарантированного сохранения порядка необходимо использовать метод AsOrdered, который после агрегирования результатов выполняет восстановление порядка.
var q = "abcdefgh".AsParallel() .Select(c=>Char.ToUpper(c)).AsOrdered().ToArray();
Обязательное упорядочивание результатов приводит к ухудшению эффективности распараллеливания. Модификатор AsUnordered позволяет снять требование сохранения порядка и повысить эффективность распараллеливания.
Разделение данных
Разбиение элементов последовательности по потокам осуществляется в соответствии с одной из трех стратегий: блочное разделение (chunk partitioning), разделение по диапазону (range partitioning), хэш-секционирование (hash sectioning).
Хэш-секционирование требует расчета хэш-значений для всех элементов последовательности; элементы с одинаковыми хэш-значениями обрабатываются одним и тем же потоком. Хэш-секционирование выполняется для операторов, сравнивающих элементы: GroupBy, Join, GroupJoin, Intersect, Except, Union, Distinct.
При разделении по диапазону последовательность разбивается на равное число элементов, каждая порция обрабатывается в одном рабочем потоке. Такое разделение является достаточно эффективным, так как приводит к полной независимости обработки элементов на разных потоках и не требует какой-либо синхронизации. Недостатком такой декомпозиции является несбалансированность загруженности потоков в случае разных вычислительных затрат при обработке элементов последовательности.
Разная вычислительная нагрузка при обработке элементов приводит к несбалансированности загрузки потоков.
Динамическое разделение данных позволяет получить более равномерную загрузку потоков, но требуют синхронизации потоков для доступа к исходной последовательности.
При динамическом (блочном) разделении каждый поток, участвующий в обработке, получает по фиксированной порции элементов (chunk). В качестве порции может быть и один элемент. После обработки своей порции поток обращается за следующей порцией.
По умолчанию при выполнении PLINQ-запросов выполняется разделение по диапазону, кроме запросов, требующих хэш-секционирования. Для выполнения запросов с динамическим разделением необходимо использовать объект Partitioner.
var parNumbers = ParallelEnumerable.Range(1, 1000); // Range-partition var q1 = (from n in parNumbers where n % 3 == 0 select n * n).ToArray(); // Range-partition double[] ard = new double[] {3.4, 56565.634, 7.8, 9.9, 2.4}; var q2 = ard.AsParallel().Select(d => Math.Sqrt(d)).ToArray(); // Block-partition var q3 = Partitioner.Create(ard, true).AsParallel() .Select(d=>Math.Sqrt(d)).ToArray();
Первый и второй запросы используют разделение по диапазону. Третий запрос использует динамическую декомпозицию. Второй аргумент метода Partitioner.Create задает режим декомпозиции: false – разделение по диапазону, true – динамическая (блочная) декомпозиция.
Обработка исключений
Исключения, которые могут произойти при выполнении PLINQ-запросов, обрабатываются также как и в случае задач с помощью объекта AggregateException в блоке catch. Блок try располагается там, где осуществляется фактический вызов обработки элементов.
var q = numbers.Select(n => SomeWork(n)).Where(n => { if(n > 0) return true; else throw new Exception(); }); try { foreach(int n in q) Console.WriteLine(n); } catch(AggregateException ae) { Console.WriteLine("Some error was happened!"); return true; }
Обработка элементов начинается при переборе элементов в foreach-цикле. Исключение обрабатывается в catch-блоке. Объект AggregateException содержит список ошибок, возникнувших при обработке элементов, в списке InnerExceptions. Для обработки исключений можно применять методы Flatten и Handle.
Отмена запроса
Для отмены выполнения PLINQ-запросов используется объект CancellationToken, который передается с помощью метода WithCancellation.
CancellationTokenSource cts = new CancellationTokenSource(); var q = someData.AsParallel().WithCancellation(cts.Token) .Select(d => d * d); // Задача, которая отменит запрос Task t = Task.Factory.StartNew(() => { Thread.Sleep(100); cts.Cancel(); }); try { var results = data.ToList(); } catch(OperationCanceledException e) { Console.WriteLine("The query was cancelled!"); }
В этом фрагменте отмена запроса осуществляется с помощью отдельной задачи. При отмене генерируется исключение OperationCanceledException, которое при необходимости можно обработать.
Агрегирование вычислений
Под агрегированием (редукцией) понимается вычисление какого-либо итогового значения для исходного набора элементов. Примером редукции являются: вычисление минимального значения, максимального значения, суммы, произведения и т.д.
Для получения агрегированных результатов можно использовать методы: Sum, Min, Max для вычисления суммы, минимального и максимального значения.
Реализация произвольных агрегированных функций осуществляется с помощью метода Aggregate. В следующем фрагменте реализован метод вычисления среднеквадратичного отклонения
double CalcStdDevParallel(double[] data) { double mean = data.AsParallel().Average(); double stdDev = data.AsParallel().Aggregate( // Инициализация локальной переменной 0.0, // Вычисления в каждом потоке (subtotal, item) => subtotal + Math.Pow(item - mean, 2), // Агрегирование локальных значений (total, subtotal) => total + subtotal, // Итоговое преобразование (total) => Math.Sqrt(total/(data.Length – 1)) ); return stdDev; }
Первый аргумент – делегат инициализации локальных переменных потоков, который вызывается один раз для каждого потока перед началом вычислений.
() => 0.0;
Второй аргумент – делегат обработки каждого элемента, принимающий в качестве параметров текущее значение локальной переменной и значение обрабатываемого элемента; возвращается значение локальной переменной. Делегат обработки вызывается для каждого элемента.
(local, item) => local + item;
Третий аргумент метода агрегирования принимает делегат редукции, определяющий, как частные переменные будут сворачиваться (редуцироваться) в одно итоговое значение. Делегат редукции принимает текущее значение итоговой переменной, локальное значение переменной и возвращает обновленное значение итоговой переменной.
(total, local) => total + local;
Делегат редукции вызывается столько раз, сколько потоков участвует в обработке метода агрегирования.
Четвертый аргумент – делегат финальной обработки итоговой переменной. Делегат вызывается только один раз после завершения редукции локальных переменных.
(total) => Math.Sqrt(total) / (data.Length – 1)
Вопросы
- Почему LINQ-запросы не распараллеливаются автоматически?
- С какой целью используется оператор возвращения к последовательному выполнению запроса AsSequential?
- Почему при выполнении параллельного запроса порядок элементов может сохраниться?
Упражнения
- Составьте запросы, которые демонстрируют неэффективность статической декомпозиции.
- Исследуйте эффективность выполнения PLINQ-запросов и шаблона Parallel.For.
- Составьте запрос, с помощью которого можно убедиться в параллельности или последовательности его выполнения.
- Исследуйте эффективность выполнения запроса с разными режимами буферизации.