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