Опубликован: 15.10.2009 | Доступ: свободный | Студентов: 896 / 248 | Оценка: 4.42 / 4.20 | Длительность: 08:22:00
Специальности: Программист
Лекция 4:

Конструкция Parallel.Invoke

< Лекция 3 || Лекция 4: 12 || Лекция 5 >
Аннотация: В лекции рассматривается еще один способ распараллеливания процессов - Parallel.Invoke, а также описаны проблемы при реализации параллельной обработки бинарных деревьев.

В "Лекции 2" были рассмотрены параллельные реализации циклов For и ForEach. Еще один способ распараллеливания, поддерживаемый классом Parallel - это метод Parallel.Invoke.

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

Подобные ситуации часто возникают в рекурсивных алгоритмах и алгоритмах типа "разделяй и властвуй". Рассмотрим, например, обход бинарного дерева:

class Tree<T>
{
public T Data;
public Tree<T> Left, Right;
…
}

На C# обход дерева в последовательной реализации может выглядеть следующим образом:

static void WalkTree<T>(Tree<T> tree, Action <T> func)
{
  if (tree == null) return;
  WalkTree(tree.Left, func);
  WalkTree(tree.Right, func);
  func(tree.Data);
}

Заметим, что в последовательной реализации, имеется группа операторов, выполняющих обход обоих ветвей дерева и работающая с текущим узлом. Если наша цель - выполнить это действие для каждого узла в дереве и если порядок, в котором эти узлы будут обслужены неважен, то мы можем распараллелить исполнение группы таких операторов, используя Parallel.Invoke. Параллельная реализация выглядит следующим образом:

static void WalkTree<T>(Tree<T> tree, Action <T> func)
{
  if (tree = null) return;
Parallel.Invoke(
    () => WalkTree(tree.Left, func);
    () => WalkTree(tree.Right, func);
    () => func(tree.Data));
}

Только что показанный способ распараллеливания применим и к другим алгоритмам типа "разделяй и властвуй". Рассмотрим последовательную реализацию алгоритма быстрой сортировки:

static void SeqQuickSort<T>(T[] domain, int left, int right)
where T : IComparable<T>
{
  if (right - left + 1 <= INSERTION_TRESHOLD)
{
  InsertionSort(domain, left, right);
}
else
{
  int pivot = Partition(domain, left, right);
  SeqQuickSort(domain, left, pivot - 1);
SeqQuickSort(domain, pivot + 1, right);
}
}

Также как в предыдущем примере, распараллеливание может быть выполнено посредством метода Parallel.Invoke:

static void ParQuickSort<T> (T[] domain, int left, int right)
where T : IComparable<T>
{
  if (right - left + 1 <= SEQUENTIAL_TRESHOLD)
{
  SeqQuickSort (domain, left, right);
}
else
{
  int pivot = Partition(domain, left, right);
Parallel.Invoke(
    () => SeqQuickSort(domain, left, pivot - 1);
() => SeqQuickSort(domain, pivot + 1, right));
}
}

Заметим, что в последовательной реализации SeqQuickSort, если размер сортируемого сегмента массива достаточно мал, то алгоритм вырождается в алгоритм сортировки вставкой ( InsertionSort ). Для очень больших массивов алгоритм быстрой сортировки значительно эффективнее, чем простые алгоритмы сортировки (сортировка вставкой, метод пузырька, сортировка выборкой) . Будем использовать эту идею и при параллельной реализации. Массив очень большого размера, поступающий на вход, разделяется на сегменты, которые обрабатываются параллельно. Однако для массива небольшого размера дополнительные издержки на обслуживание потоков могут привести к потере производительности. Итак, если для массивов небольшого размера SeqQuickSort вырождается в InsertionSort, то в параллельном варианте ParQuickSort выраждается в SeqQuickSort. Аналогичным способом можно организовать только что рассмотренный обход бинарного дерева, чтобы уменьшить потери производительности:

static void WalkTree<T> (Tree<T> tree, Action<T> func, int depth)
{
  if (tree = null) return;
else if (depth > SEQUENTIAL_TRESHOLD)
{
    WalkTree(tree.Left, func, depth + 1);
    WalkTree(tree.Right, func, depth + 1);
    func(tree.Data);
}
else
{
Parallel.Invoke(
      () => WalkTree(tree.Left, func, depth + 1);
      () => WalkTree(tree.Right, func, depth + 1);
      () => func(tree.Data));
}
}
< Лекция 3 || Лекция 4: 12 || Лекция 5 >
Максим Полищук
Максим Полищук
"...Изучение и анализ примеров.
В и приведены описания и приложены исходные коды параллельных программ..."
Непонятно что такое - "В и приведены описания" и где именно приведены и приложены исходные коды.