"...Изучение и анализ примеров.
В и приведены описания и приложены исходные коды параллельных программ..." Непонятно что такое - "В и приведены описания" и где именно приведены и приложены исходные коды. |
Примеры программирования с использованием библиотеки PFX
Семинарское занятие № 9. Рекурсия и параллелизм (часть 3)
Одной из целей при проектировании библиотеки Parallel FX для .NET Framework было облегчить реализацию рекурсивных параллельных операций и обеспечить для них максимально возможную эффективность.
В частности, используя средства PLINQ, обход и обработка вершин дерева записывается в виде нескольких строк кода. Используя итератор класса Tree<T>, который был реализован в примере семинара 8, метод Process запишется следующим образом:
public static void Process<T> (Tree<T> tree, Action<T> action) { if (tree == null) return; GetNodes(tree).AsParallel().ForAll(action); }Пример 9.1.
Внутренние действия, производимые библиотекой PFX при реализации данного PLINQ-фрагмента, очень похожи на действия из примера семинара, в котором создаются несколько потоков, которые выбирают вершины для обработки с помощью конструкции перечисления, используя механизм запирания ( lock ). Однако, в PLINQ такой подход реализован более эффективно путем увеличения количества вершин, извлекаемых из перечисления за один раз, что минимизирует использование конструкции lock (см. Задачу 7).
На самом деле, как и в ранее приведенных реализациях, в пример 9.1 осуществляется последовательный проход по дереву с запуском действий обработки ( actions ) в асинхронном режиме. Чтобы реализовать параллельный проход по дереву, когда различные поддеревья обрабатываются различными потоками, можно воспользоваться библиотекой TPL ( Task Parallel Library ):
public static void Process<T> (Tree<T> tree, Action<T> action) { if (tree == null) return; Parallel.Invoke( () => action(tree.Data), () => Process(tree.Left, action), () => Process(tree.Right, action)); }Пример 9.2.
Оператор Parallel.Invoke потенциально запускает параллельное исполнение всех трех операторов из данного примера, заканчивая свою работу только по завершении их всех. Отличие от аналогичного примера, использующего класс ThreadPool, состоит в том, что здесь не может наступить состояние дедлока ввиду нехватки потоков для исполнения, поскольку реализация TPL по максимуму использует текущий (главный) поток для выполнения в нем обработки, которая, при наличии свободных потоков, исполнялась бы ими.
Если необходимо более гибкое управление параллельным исполнением отдельных фрагментов кода, то они могут быть оформлены в виде задач:
public static void Process<T> (Tree<T> tree, Action<T> action) { if (tree == null) return; var t1 = Task.Create( delegate { Process(tree.Left, action); }); var t2 = Task.Create( delegate { Process(tree.Right, action); }); action(tree.Data); Task.WaitAll(new Task[] { t1, t2 }); }Пример 9.3.
Аналогичные методы могут быть применены к реализации алгоритма быстрой сортировки:
public static void Quicksort<T> (T[] arr, int left, int right) where T : IComparable<T> { if (right > left) { int pivot = Partition(arr, left, right); Parallel.Invoke( () => Quicksort(arr, left, pivot - 1), () => Quicksort(arr, pivot + 1, right)); } }Пример 9.4.
а также к реализации алгоритма сортировки слиянием:
public static void Mergesort<T> ( T[] arr, int left, int right) where T : IComparable<T> { if (right > left) { int mid = (right + left) / 2; Parallel.Invoke( () => Mergesort(arr, left, mid), () => Mergesort(arr, mid + 1, right)); Merge(arr, left, mid + 1, right); } }Пример 9.5.
Хотя параллельные версии этих алгоритмов мы получили очень просто, однако, этот код, по-прежнему, имеет проблемы. Реализация конструкции Parallel.Invoke построена на создании задач (см. пример 9.3) для отдельных операций и ожидании завершения работы этих задач. Если вычислительная сложность отдельных операций мала, то накладные расходы на создание задач и ожидание завершения их работы будут велики по сравнению с основными вычислительными операциями. Это может приводить к тому, что параллельная реализация будет работать медленнее, чем последовательная.
Для того, чтобы решить эту проблему, необходимо немного усложнить код, применив широко известный механизм на базе использования порога ( threshold ). Идея применения порога состоит в том, что мы вводим параллелизм в исполнение программы до тех пор, пока не будут загружены достаточной работой все ядра (процессоры), после чего происходит переключение на последовательное исполнение работ в рамках запущенных параллельных потоков, что позволяет избежать дополнительных накладных расходов. Например, в случае обхода дерева, используя понятие глубины вершины дерева в качестве порога, метод Process может выглядеть следующим образом:
private static void Process<T> ( Tree<T> tree, Action<T> action, int depth) { if (tree == null) return; if (depth > 5) { action(tree.Data); Process(tree.Left, action, depth + 1); Process(tree.Right, action, depth + 1); } else { Parallel.Invoke( () => action(tree.Data), () => Process(tree.Left, action, depth + 1), () => Process(tree.Right, action, depth + 1)); } }Пример 9.6.
В пример 9.6 присутствуют одновременно последовательная и параллельная реализации обхода дерева, а переключение с одной реализации на другую происходит в зависимости от глубины вершины, которая обрабатывается в текущий момент. Следует, однако, заметить, что сама по себе глубина не всегда может служить эффективным порогом - хорошей здесь иллюстрацией являются несбалансированные деревья. Тем не менее, использование порогов может существенно повысить эффективность реализации параллельных алгоритмов сортировки (см. Задачу 9).
Задача 10.
Реализуйте алгоритмы Quicksort и Mergesort с помощью конструкции Parallel.Invoke, используя механизм порогов.