Кластерные вычисления

Программирование на языке MC#

Разбить на страницы
Показывать лекцию целиком

В этом разделе, использование специфических конструкций языка MC# будет проиллюстрировано на ряде параллельных и распределенных программ. Также излагаются и иллюстрируются общие принципы построения MC#-программ для нескольких типичных задач параллельного программирования. Подчеркиваются различия при разработке параллельных программ, предназначенных для исполнения на системах с общей памятью (например, мультиядерных процессорах), и распределенных программ, исполняющихся на сети машин (вычислительном кластере).

5.1. Вычисление чисел Фибоначчи

Последовательность чисел Фибоначчи есть бесконечный ряд из натуральных чисел

a0, a1, a2, a3, . . .

таких, что

a0 = 1,
a1 = 1, и
ai = ai-1 + ai-2,  для  i >=  2.

Построим параллельную программу, находящую n -ое ( n >= 0 ) число в ряду Фибоначчи, т.е., элемент an последовательности.

Первый вариант такой программы будет иметь рекурсивную структуру, соответствующую формуле определения чисел Фибоначчи. Основной вычислительный метод этой программы будет объявлен асинхронным, и будет возвращать вычисленный результат по каналу, переданному ему в качестве одного из входных аргументов. С другой стороны, в рекурсивных вызовах внутри этого метода будут использоваться каналы для получения значений от методов, вызванных рекурсивно.

Класс Fib, содержащий основной вычислительный метод Compute, может иметь следующий вид:

Главный класс для этой программы может выглядеть следующим образом:

Упражнение 1. Показать, почему при рекурсивных вызовах функции Compute необходимо создание новых объектов класcа Fib.

( Подсказка: рассмотреть как будут использоваться каналы ic1 и ic2 в программе, в которой опущено создание таких объектов, например, при вызове функции Compute с n = 3).

Легко видеть, что приведенный вариант параллельной программы является очень неэффективным, поскольку в нем порождается 2n асинхронных вызовов метода Compute, каждый из которых выполняет очень мало вычислительных операций: фактически, порождает два дополнительных вызова и передает результат по каналу. Очевидно, что в этом случае эффект от параллельного исполнения методов будет перекрыт накладными расходами на их порождение.

Избежать порождения асинхронных вызовов функции Compute для малых по величине аргументов n можно путем введения "локального" вычисления соответствующей функции для малых n:

Приведенный выше вариант является более эффективным, чем первый, но и он обладает существенным недостатком: теперь async-методы для больших n выполняют очень мало вычислительных операций.

Сократить общее количество порождаемых рекурсивно async-методов и более равномерно распределить по ним вычислительную нагрузку позволяет следующий, "линейный" вариант программы Fib:

Приведенные выше рассуждения и варианты программы Fib переносятся и на варианты этой же программы, но для распределенного исполнения (т.е., с заменой async на movable ). При этом, для получения максимального ускорения программист должен подобрать оптимальное значение константы threshold.

Ниже приведены графики времени выполнения последовательной программы и распределенного, "линейного" варианта программы Fib с threshold = 36. Причем количество используемых процессоров в распределенном варианте определялось как n - 34 (для n >= 35 ). Тестовые замеры проводились на кластере с процессорами AMD Athlon(TM) MP 1800+.

(рис 5.1) Время вычисления N-го числа Фибоначчи "линейным" алгоритмом.

Полные тексты вариантов программы Fib приведёны в приложении [A1].

5.2. Обход бинарного дерева

Если структура данных задачи организована в виде дерева, то его обработку легко распараллелить путем обработки каждого поддерева отдельном async- ( movable- ) методом.

Предположим, что мы имеем следующее определение (в действительности, сбалансированного) бинарного дерева в виде класса BinTree:

Тогда просуммировать значения, находящиеся в узлах такого дерева (и, в общем случае, произвести более сложную обработку) можно с помощью следующей программы, структура которой, по существу, совпадает со структурой предыдущей программы Fib:

Естественно, что для повышения эффективности этой программы, а именно, для получения ускорения при исполнении ее на параллельной архитектуре, необходимо внести в нее усовершенствования, аналогичные тем, которые были сделаны для программы Fib.

Следует также отметить, что в случае распределенного варианта этой программы, при вызове movable -метода Sum, к объекту класса BinTree, являющемуся аргументом этого метода, будут применяться процедуры сериализации/десериализации при переносе вычислений на другой компьютер. (В действительности, с точки зрения Runtime-языка MC#, поддерживающей распределенное исполнение программ, канал также является обычным объектом, к которому будут применяться процедуры сериализации/десериализации).

Полный текст данной программы приведён в приложении [A2].

Упражнение 2. Предположим, что имеется класс Tree, внутренними полями которого являются поле value, хранящее значение, связанное с корнем данного дерева, и поле subtrees, являющееся массивом объектов класса Tree.

Написать параллельную программу на MC#, суммирующую значения из всех вершин заданного дерева.

( Подсказка: для получения значений от рекурсивно порождаемых методов, воспользуйтесь одним каналом).

5.3. Быстрое преобразование Фурье

Напомним коротко некоторые понятия и определения, относящиеся к дискретному преобразованию Фурье и быстрым алгоритмам его вычисления. Более подробно об этом см., например, в главе 32.2 "Дискретное преобразование Фурье. Быстрый алгоритм" книги [6].

Комплексное число

$$\omega_n=e^{2\pi i/n}$$

называется главным значением корня степени n из единицы.

Вектор y=(y0, y1, … , yn-1) называется дискретным преобразованием Фурье вектора a=(a0, a1, … , an-1), где ai и yj ( 0 <= i, j <= n ) есть комплексные числа, если

$$y_k=\sum_{0\le j\le n-1} a_j\; \omega_n^{kj}\\ \mbox{для}\quad k=0,1,\ldots,n-1.$$

Быстрое преобразование Фурье (БПФ) представляет собой метод быстрого вычисления дискретного преобразования Фурье, использующий свойства комплексных корней из единицы и требующий времени O(n log n), в отличие от времени O(n2) при прямом вычислении по формуле.

В случае, когда n есть степень двойки, имеется следующий алгоритм быстрого преобразования Фурье вектора a=(a0, a1, … , an-1):

Легко видеть, что используемый в данном алгоритме двоичный принцип "разделяй и властвуй" аналогичен принципам, реализованным в предыдущих программах вычисления чисел Фибоначчи и обхода двоичного дерева.

Ниже приведен текст на языке MC# асинхронного метода, реализующего приведенный выше рекурсивный алгоритм. Полный текст этой программы можно найти в приложении [A3].

Однако, данная программа, даже с оптимизациями, аналогичными рассмотренным для программ Fib и SumBinTree, не дает ускорения при исполнении на параллельной архитектуре из-за больших объемов требуемой памяти ( в 2 раза бoльших, чем для последовательного варианта), необходимой для размещения массивов a[0], a[1], y[0], y[1] при рекурсивных вызовах.

Существуют более эффективные алгоритмы БПФ, например, итеративный алгоритм из главы 32.3 "Эффективные реализации быстрого преобразования Фурье" из книги [A3].

Ниже представлены графики времени исполнения последовательного и итеративного параллельного (на 2 процессора) алгоритмов БПФ, реализованных на языке MC#.

Тестовые замеры проводились на машине с двухядерным процессором Intel Core 2 Duo 2.4 GHz и оперативной памятью 1 Гб.

(рис 5.2) Время исполнения последовательного и параллельного алгоритмов БПФ.

5.4. Построения списка простых чисел методом "решета Эратосфена"

В данном разделе будет представлена параллельная программа построения списка простых чисел методом просеивания (другое название этого метода - "решето Эратосфена").

5.4.1. Наивный алгоритм

По условию задачи, по заданному натуральному числу N >= 2, необходимо найти все простые числа в интервале от 2 до N.

Метод просеивания состоит из следующих шагов:

  • из исходного списка l0 всех натуральных чисел от 2 до N

    l0 = [2, … , N]

    выбирается первое число этого списка, а именно 2, и выдается в качестве первого выходного значения;
  • затем строится новый список l1, который получается из списка l0 вычеркиванием из него всех чисел, кратных очередному выбранному простому числу - на первом шаге, числу 2:

    l1 = [3, 5, 7, … , N] (в предположении, что N нечетно)

  • затем данная процедура повторяется рекурсивно для вновь построенного списка.
  • Алгоритм заканчивает свою работу, когда очередной список оказывается пустым.

    В соответствии со сказанным выше, основная вычислительная процедура программы (метод Sieve), тем самым, должна производить следующие действия:

  • переслать первый элемент из входного потока натуральных чисел (если он не пуст) в выходной поток;
  • отфильтровать все оставшиеся числа их входного потока по модулю первого элемента и направить этот отфильтрованный поток на вход новой рекурсивно вызванной процедуре Sieve; выходным потоком для рекурсивно вызванной процедуры становится исходный выходной поток.
  • Выходным же каналом для этой новой процедуры будет исходный выходной канал.

    Графически, один шаг рекурсивного развертывания программы может быть представлен следующим образом :

    (рис 5.3) Шаг рекурсивного развертывания программы.

    Рекурсивные вызовы метода Sieve сопровождаются созданием соответствующих объектов (класса CSieve ), имеющих каналы и обработчики. Эти каналы и обработчики используются для создания цепочки обрабатывающих элементов, с помощью которых производится просеивание потока натуральных чисел.

    Программа состоит из следующих методов:

  • async Sieve( handler int() getList, channel ( int ) sendPrime) - async-метод класса CSieve, который реализует собственно алгоритм просеивания: из обработчика getList предыдущего объекта цепочки читается поток чисел, фильтруется по модулю первого элемента из потока, и выбранные простые числа посылаются в канал sendPrime (через дальнейшие рекурсивные вызовы метода Sieve );
  • void filter ( int p, handler int()getList, channel ( int ) cfiltered) - функция класса CSieve, реализующая фильтрацию потока натуральных чисел: из потока чисел, получаемых путем вызова обработчика getList, удаляются все элементы, которые делятся на число p, и посылаются по каналу cfiltered;
  • public static void Main( String [] args )- главная функция из класса Eratosthenes, которая создает поток натуральных чисел Nats, и, по мере получения простых чисел из обработчика getPrime, выводит их на консоль.
  • Полный текст программы приведен ниже, а также в приложении [A4].

    Как обычно, распределенный вариант программы, который может исполняться на сети машин, получается заменой ключевого слова async на movable.

    В принципе, при исполнении программы на одной машине, элементы цепочки, с помощью которой производится просеивание, могут быть построены из стандартных объектов .NET, например, из очередей (объектов класса Queue ). Однако, в этом случае, программист должен позаботиться об их блокировке, что необходимо в случае одновременной работы множества синхронных методов (потоков). Использование каналов и обработчиков языка MC# освобождает программиста от этой обязанности.

    Аналогично ранее рассмотренным примерам, метод Sieve выполняет слишком мало вычислительных операций, чтобы обеспечить эффективность всей параллельной (распределенной ) программы в целом.

    5.4.2. Пакетный алгоритм

    В данном разделе описывается модификация наивного алгоритма "решето Эратосфена", существенно улучшающая эффективность параллельной (распределенной) программы, и которая дает существенное ускорение при поиске простых чисел в длинных интервалах (например, для N >= 106 ).

    Основная идея предлагаемого алгоритма заключается в переходе от использования потоков одиночных данных (потоков отдельных натуральных чисел) к потокам пакетов натуральных чисел.

    Пакет - это массив натуральных чисел фиксированного размера (задаваемого в программе значением PACKAGE_SIZE ), пустые хвостовые элементы которого заполняются нулями.

    При этом, функции наивного алгоритма обобщаются в данном варианте естественным образом:

  • async Sieve( handler int []() getNatPack, channel (int []) sendPrimesPack) - с помощью обработчика getNatPack получает первый пакет из потока, обрабатывает его функцией SynchronousSieve, получая пакет простых чисел head, и отправляя его по каналу sendPrimesPack; остальные пакеты из входного потока фильтруются с помощью функции filter по модулю пакета head и направляются на вход следующей в цепочке рекурсивной функции Sieve ;
  • void filter( int [] head, handler int [] () getNatPack, channel ( int []) cfiltered) - функция, которая фильтрует пакеты из входного потока, получаемые с помощью обработчика getNatPack, по модулю пакета простых чисел head, отправляя результирующие пакеты в канал cfiltered; при этом, все результирующие пакеты (кроме, может быть, последнего) имеют длину PACKAGE_SIZE (строго говоря, и последний пакет имеет длину PACKAGE_SIZE, но его хвостовая часть может быть заполнена нулями).
  • Полный текст программы Eratosthenes2 приведен ниже, а также в приложении [A4].

    5.5. Программа all2all

    Программа all2all предназначена для демонстрации способа, с помощью которого можно обеспечить взаимодействие внутри множества асинхронных (распределенных) процессов в соответствии с принципом "все со всеми". В определенном смысле, эта программа показывает, как можно реализовать на языке MC# глобальные (в терминах MPI, "широковещательные"(broadcast) ) операции передачи данных. Данный подход часто используется в программах, реализующих параллелизм по данным, когда отдельные процессы (процессоры) должны обмениваться сообщениями как со своими соседями, так и со всеми процессами, участвующими в вычислениях.

    Ниже будет представлен вариант программы all2all с распределенными ( movable- ) процессами.

    В этой программе, каждый распределенный процесс представляется методом Start объекта класса DistribProcess. Для взаимодействия между собой, каждый распределенный процесс создает объект класса BDChannel (Bidirectional channel), содержащий канал Send и обработчик Receive:

    Обменявшись между собой такими объектами, распределенные процессы могут посылать и принимать сообщения друг от друга независимо от их физического расположения. Обмен объектами класса BDChannel реализуется через главный процесс, который исполняется на машине, где исходно было запущено распределенное приложение:

    Каждый распределенный процесс (объект класса DistribProcess ) выполняет в данной программе следующую последовательность действий:

  • Создает свой собственный объект класса BDChannel и отсылает его главному процессу.
  • Принимает от главного процесса массив объектов класса BDChannel всех остальных процессов (включая свой собственный).
  • Пользуясь этим массивом, посылает сообщение всем остальным процессам в группе.
  • Принимает сообщения от всех процессов в группе.
  • Посылает сигнал об окончании своей работы главному процессу.
  • Полный текст программы All2all приведен в приложении [A5].

    Страницы:

    В этом разделе, использование специфических конструкций языка MC# будет проиллюстрировано на ряде параллельных и распределенных программ. Также излагаются и иллюстрируются общие принципы построения MC#-программ для нескольких типичных задач параллельного программирования. Подчеркиваются различия при разработке параллельных программ, предназначенных для исполнения на системах с общей памятью (например, мультиядерных процессорах), и распределенных программ, исполняющихся на сети машин (вычислительном кластере).

    5.1. Вычисление чисел Фибоначчи

    Последовательность чисел Фибоначчи есть бесконечный ряд из натуральных чисел

    a0, a1, a2, a3, . . .

    таких, что

    a0 = 1,
    a1 = 1, и
    ai = ai-1 + ai-2,  для  i >=  2.

    Построим параллельную программу, находящую n -ое ( n >= 0 ) число в ряду Фибоначчи, т.е., элемент an последовательности.

    Первый вариант такой программы будет иметь рекурсивную структуру, соответствующую формуле определения чисел Фибоначчи. Основной вычислительный метод этой программы будет объявлен асинхронным, и будет возвращать вычисленный результат по каналу, переданному ему в качестве одного из входных аргументов. С другой стороны, в рекурсивных вызовах внутри этого метода будут использоваться каналы для получения значений от методов, вызванных рекурсивно.

    Класс Fib, содержащий основной вычислительный метод Compute, может иметь следующий вид:

    Главный класс для этой программы может выглядеть следующим образом:

    Упражнение 1. Показать, почему при рекурсивных вызовах функции Compute необходимо создание новых объектов класcа Fib.

    ( Подсказка: рассмотреть как будут использоваться каналы ic1 и ic2 в программе, в которой опущено создание таких объектов, например, при вызове функции Compute с n = 3).

    Легко видеть, что приведенный вариант параллельной программы является очень неэффективным, поскольку в нем порождается 2n асинхронных вызовов метода Compute, каждый из которых выполняет очень мало вычислительных операций: фактически, порождает два дополнительных вызова и передает результат по каналу. Очевидно, что в этом случае эффект от параллельного исполнения методов будет перекрыт накладными расходами на их порождение.

    Избежать порождения асинхронных вызовов функции Compute для малых по величине аргументов n можно путем введения "локального" вычисления соответствующей функции для малых n:

    Приведенный выше вариант является более эффективным, чем первый, но и он обладает существенным недостатком: теперь async-методы для больших n выполняют очень мало вычислительных операций.

    Сократить общее количество порождаемых рекурсивно async-методов и более равномерно распределить по ним вычислительную нагрузку позволяет следующий, "линейный" вариант программы Fib:

    Приведенные выше рассуждения и варианты программы Fib переносятся и на варианты этой же программы, но для распределенного исполнения (т.е., с заменой async на movable ). При этом, для получения максимального ускорения программист должен подобрать оптимальное значение константы threshold.

    Ниже приведены графики времени выполнения последовательной программы и распределенного, "линейного" варианта программы Fib с threshold = 36. Причем количество используемых процессоров в распределенном варианте определялось как n - 34 (для n >= 35 ). Тестовые замеры проводились на кластере с процессорами AMD Athlon(TM) MP 1800+.

    (рис 5.1) Время вычисления N-го числа Фибоначчи "линейным" алгоритмом.

    Полные тексты вариантов программы Fib приведёны в приложении [A1].

    5.2. Обход бинарного дерева

    Если структура данных задачи организована в виде дерева, то его обработку легко распараллелить путем обработки каждого поддерева отдельном async- ( movable- ) методом.

    Предположим, что мы имеем следующее определение (в действительности, сбалансированного) бинарного дерева в виде класса BinTree:

    Тогда просуммировать значения, находящиеся в узлах такого дерева (и, в общем случае, произвести более сложную обработку) можно с помощью следующей программы, структура которой, по существу, совпадает со структурой предыдущей программы Fib:

    Естественно, что для повышения эффективности этой программы, а именно, для получения ускорения при исполнении ее на параллельной архитектуре, необходимо внести в нее усовершенствования, аналогичные тем, которые были сделаны для программы Fib.

    Следует также отметить, что в случае распределенного варианта этой программы, при вызове movable -метода Sum, к объекту класса BinTree, являющемуся аргументом этого метода, будут применяться процедуры сериализации/десериализации при переносе вычислений на другой компьютер. (В действительности, с точки зрения Runtime-языка MC#, поддерживающей распределенное исполнение программ, канал также является обычным объектом, к которому будут применяться процедуры сериализации/десериализации).

    Полный текст данной программы приведён в приложении [A2].

    Упражнение 2. Предположим, что имеется класс Tree, внутренними полями которого являются поле value, хранящее значение, связанное с корнем данного дерева, и поле subtrees, являющееся массивом объектов класса Tree.

    Написать параллельную программу на MC#, суммирующую значения из всех вершин заданного дерева.

    ( Подсказка: для получения значений от рекурсивно порождаемых методов, воспользуйтесь одним каналом).

    5.3. Быстрое преобразование Фурье

    Напомним коротко некоторые понятия и определения, относящиеся к дискретному преобразованию Фурье и быстрым алгоритмам его вычисления. Более подробно об этом см., например, в главе 32.2 "Дискретное преобразование Фурье. Быстрый алгоритм" книги [6].

    Комплексное число

    $$\omega_n=e^{2\pi i/n}$$

    называется главным значением корня степени n из единицы.

    Вектор y=(y0, y1, … , yn-1) называется дискретным преобразованием Фурье вектора a=(a0, a1, … , an-1), где ai и yj ( 0 <= i, j <= n ) есть комплексные числа, если

    $$y_k=\sum_{0\le j\le n-1} a_j\; \omega_n^{kj}\\ \mbox{для}\quad k=0,1,\ldots,n-1.$$

    Быстрое преобразование Фурье (БПФ) представляет собой метод быстрого вычисления дискретного преобразования Фурье, использующий свойства комплексных корней из единицы и требующий времени O(n log n), в отличие от времени O(n2) при прямом вычислении по формуле.

    В случае, когда n есть степень двойки, имеется следующий алгоритм быстрого преобразования Фурье вектора a=(a0, a1, … , an-1):

    Легко видеть, что используемый в данном алгоритме двоичный принцип "разделяй и властвуй" аналогичен принципам, реализованным в предыдущих программах вычисления чисел Фибоначчи и обхода двоичного дерева.

    Ниже приведен текст на языке MC# асинхронного метода, реализующего приведенный выше рекурсивный алгоритм. Полный текст этой программы можно найти в приложении [A3].

    Однако, данная программа, даже с оптимизациями, аналогичными рассмотренным для программ Fib и SumBinTree, не дает ускорения при исполнении на параллельной архитектуре из-за больших объемов требуемой памяти ( в 2 раза бoльших, чем для последовательного варианта), необходимой для размещения массивов a[0], a[1], y[0], y[1] при рекурсивных вызовах.

    Существуют более эффективные алгоритмы БПФ, например, итеративный алгоритм из главы 32.3 "Эффективные реализации быстрого преобразования Фурье" из книги [A3].

    Ниже представлены графики времени исполнения последовательного и итеративного параллельного (на 2 процессора) алгоритмов БПФ, реализованных на языке MC#.

    Тестовые замеры проводились на машине с двухядерным процессором Intel Core 2 Duo 2.4 GHz и оперативной памятью 1 Гб.

    (рис 5.2) Время исполнения последовательного и параллельного алгоритмов БПФ.

    5.4. Построения списка простых чисел методом "решета Эратосфена"

    В данном разделе будет представлена параллельная программа построения списка простых чисел методом просеивания (другое название этого метода - "решето Эратосфена").

    5.4.1. Наивный алгоритм

    По условию задачи, по заданному натуральному числу N >= 2, необходимо найти все простые числа в интервале от 2 до N.

    Метод просеивания состоит из следующих шагов:

  • из исходного списка l0 всех натуральных чисел от 2 до N

    l0 = [2, … , N]

    выбирается первое число этого списка, а именно 2, и выдается в качестве первого выходного значения;
  • затем строится новый список l1, который получается из списка l0 вычеркиванием из него всех чисел, кратных очередному выбранному простому числу - на первом шаге, числу 2:

    l1 = [3, 5, 7, … , N] (в предположении, что N нечетно)

  • затем данная процедура повторяется рекурсивно для вновь построенного списка.
  • Алгоритм заканчивает свою работу, когда очередной список оказывается пустым.

    В соответствии со сказанным выше, основная вычислительная процедура программы (метод Sieve), тем самым, должна производить следующие действия:

  • переслать первый элемент из входного потока натуральных чисел (если он не пуст) в выходной поток;
  • отфильтровать все оставшиеся числа их входного потока по модулю первого элемента и направить этот отфильтрованный поток на вход новой рекурсивно вызванной процедуре Sieve; выходным потоком для рекурсивно вызванной процедуры становится исходный выходной поток.
  • Выходным же каналом для этой новой процедуры будет исходный выходной канал.

    Графически, один шаг рекурсивного развертывания программы может быть представлен следующим образом :

    (рис 5.3) Шаг рекурсивного развертывания программы.

    Рекурсивные вызовы метода Sieve сопровождаются созданием соответствующих объектов (класса CSieve ), имеющих каналы и обработчики. Эти каналы и обработчики используются для создания цепочки обрабатывающих элементов, с помощью которых производится просеивание потока натуральных чисел.

    Программа состоит из следующих методов:

  • async Sieve( handler int() getList, channel ( int ) sendPrime) - async-метод класса CSieve, который реализует собственно алгоритм просеивания: из обработчика getList предыдущего объекта цепочки читается поток чисел, фильтруется по модулю первого элемента из потока, и выбранные простые числа посылаются в канал sendPrime (через дальнейшие рекурсивные вызовы метода Sieve );
  • void filter ( int p, handler int()getList, channel ( int ) cfiltered) - функция класса CSieve, реализующая фильтрацию потока натуральных чисел: из потока чисел, получаемых путем вызова обработчика getList, удаляются все элементы, которые делятся на число p, и посылаются по каналу cfiltered;
  • public static void Main( String [] args )- главная функция из класса Eratosthenes, которая создает поток натуральных чисел Nats, и, по мере получения простых чисел из обработчика getPrime, выводит их на консоль.
  • Полный текст программы приведен ниже, а также в приложении [A4].

    Как обычно, распределенный вариант программы, который может исполняться на сети машин, получается заменой ключевого слова async на movable.

    В принципе, при исполнении программы на одной машине, элементы цепочки, с помощью которой производится просеивание, могут быть построены из стандартных объектов .NET, например, из очередей (объектов класса Queue ). Однако, в этом случае, программист должен позаботиться об их блокировке, что необходимо в случае одновременной работы множества синхронных методов (потоков). Использование каналов и обработчиков языка MC# освобождает программиста от этой обязанности.

    Аналогично ранее рассмотренным примерам, метод Sieve выполняет слишком мало вычислительных операций, чтобы обеспечить эффективность всей параллельной (распределенной ) программы в целом.

    5.4.2. Пакетный алгоритм

    В данном разделе описывается модификация наивного алгоритма "решето Эратосфена", существенно улучшающая эффективность параллельной (распределенной) программы, и которая дает существенное ускорение при поиске простых чисел в длинных интервалах (например, для N >= 106 ).

    Основная идея предлагаемого алгоритма заключается в переходе от использования потоков одиночных данных (потоков отдельных натуральных чисел) к потокам пакетов натуральных чисел.

    Пакет - это массив натуральных чисел фиксированного размера (задаваемого в программе значением PACKAGE_SIZE ), пустые хвостовые элементы которого заполняются нулями.

    При этом, функции наивного алгоритма обобщаются в данном варианте естественным образом:

  • async Sieve( handler int []() getNatPack, channel (int []) sendPrimesPack) - с помощью обработчика getNatPack получает первый пакет из потока, обрабатывает его функцией SynchronousSieve, получая пакет простых чисел head, и отправляя его по каналу sendPrimesPack; остальные пакеты из входного потока фильтруются с помощью функции filter по модулю пакета head и направляются на вход следующей в цепочке рекурсивной функции Sieve ;
  • void filter( int [] head, handler int [] () getNatPack, channel ( int []) cfiltered) - функция, которая фильтрует пакеты из входного потока, получаемые с помощью обработчика getNatPack, по модулю пакета простых чисел head, отправляя результирующие пакеты в канал cfiltered; при этом, все результирующие пакеты (кроме, может быть, последнего) имеют длину PACKAGE_SIZE (строго говоря, и последний пакет имеет длину PACKAGE_SIZE, но его хвостовая часть может быть заполнена нулями).
  • Полный текст программы Eratosthenes2 приведен ниже, а также в приложении [A4].

    5.5. Программа all2all

    Программа all2all предназначена для демонстрации способа, с помощью которого можно обеспечить взаимодействие внутри множества асинхронных (распределенных) процессов в соответствии с принципом "все со всеми". В определенном смысле, эта программа показывает, как можно реализовать на языке MC# глобальные (в терминах MPI, "широковещательные"(broadcast) ) операции передачи данных. Данный подход часто используется в программах, реализующих параллелизм по данным, когда отдельные процессы (процессоры) должны обмениваться сообщениями как со своими соседями, так и со всеми процессами, участвующими в вычислениях.

    Ниже будет представлен вариант программы all2all с распределенными ( movable- ) процессами.

    В этой программе, каждый распределенный процесс представляется методом Start объекта класса DistribProcess. Для взаимодействия между собой, каждый распределенный процесс создает объект класса BDChannel (Bidirectional channel), содержащий канал Send и обработчик Receive:

    Обменявшись между собой такими объектами, распределенные процессы могут посылать и принимать сообщения друг от друга независимо от их физического расположения. Обмен объектами класса BDChannel реализуется через главный процесс, который исполняется на машине, где исходно было запущено распределенное приложение:

    Каждый распределенный процесс (объект класса DistribProcess ) выполняет в данной программе следующую последовательность действий:

  • Создает свой собственный объект класса BDChannel и отсылает его главному процессу.
  • Принимает от главного процесса массив объектов класса BDChannel всех остальных процессов (включая свой собственный).
  • Пользуясь этим массивом, посылает сообщение всем остальным процессам в группе.
  • Принимает сообщения от всех процессов в группе.
  • Посылает сигнал об окончании своей работы главному процессу.
  • Полный текст программы All2all приведен в приложении [A5].

    Вернуться к учебному плану