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

Примеры программирования с использованием библиотеки PFX

< Лекция 8 || Лекция 9: 12 || Лекция 10 >

Семинарское занятие № 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, используя механизм порогов.

< Лекция 8 || Лекция 9: 12 || Лекция 10 >
Максим Полищук
Максим Полищук
"...Изучение и анализ примеров.
В и приведены описания и приложены исходные коды параллельных программ..."
Непонятно что такое - "В и приведены описания" и где именно приведены и приложены исходные коды.