Опубликован: 05.03.2013 | Уровень: для всех | Доступ: платный
Лекция 8:

Технология 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) 
 

Вопросы

  1. Почему LINQ-запросы не распараллеливаются автоматически?
  2. С какой целью используется оператор возвращения к последовательному выполнению запроса AsSequential?
  3. Почему при выполнении параллельного запроса порядок элементов может сохраниться?

Упражнения

  1. Составьте запросы, которые демонстрируют неэффективность статической декомпозиции.
  2. Исследуйте эффективность выполнения PLINQ-запросов и шаблона Parallel.For.
  3. Составьте запрос, с помощью которого можно убедиться в параллельности или последовательности его выполнения.
  4. Исследуйте эффективность выполнения запроса с разными режимами буферизации.
Светлана Папшева
Светлана Папшева
Россия
Валерий Субботин
Валерий Субботин
Украина