|
"...Изучение и анализ примеров.
В и приведены описания и приложены исходные коды параллельных программ..." Непонятно что такое - "В и приведены описания" и где именно приведены и приложены исходные коды. |
Конструкция 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));
}
}