"...Изучение и анализ примеров.
В и приведены описания и приложены исходные коды параллельных программ..." Непонятно что такое - "В и приведены описания" и где именно приведены и приложены исходные коды. |
Высокоуровневый язык параллельного программирования MC#
Каналы и обработчики канальных сообщений.
Каналы и обработчики канальных сообщений являются средствами для организации взаимодействия параллельных распределенных процессов между собой. Синтаксически, каналы и обработчики обычно объявляются в программе с помощью специальных конструкций - связок (chords).
В общем случае, синтаксические правила определения связок в языке MC# имеют вид:
chord-declaration ::= [handler-header] [& channel-header ]* body handler-header ::= attributes modifiers handler handler-name return-type ( formal-parameters ) channel-header ::= attributes modifiers channel channel-name ( formal-parameters )
Связки определяются в виде членов класса. По правилам корректного определения, каналы и обработчики не могут иметь модификатора static, а потому они всегда привязаны к некоторому объекту класса, в рамках которого они объявлены.
Обработчик используется для приема значений (возможно, предобработанного с помощью кода, являющегося телом связки) из канала (или группы каналов), совместно определенных с этим обработчиком. Если, к моменту вызова обработчика, связанный с ним канал пуст (т.е., по этому каналу значений не поступало или они все были выбраны посредством предыдущих обращений к обработчику), то этот вызов блокируется. Когда по каналу приходит очередное значение, то происходит исполнение тела связки (которое может состоять из произвольных вычислений) и по оператору return происходит возврат результирующего значения обработчику.
Наоборот, если к моменту прихода значения по каналу, нет вызовов обработчика, то это значение просто сохраняется во внутренней очереди канала, где, в общем случае, накапливаются все сообщения, посылаемые по данному каналу. При вызове обработчика и при наличии значений во всех каналах соответствующей связки, для обработки в теле этой связки будут выбраны первые по порядку значения из очередей каналов.
Следует отметить, что, принципиально, срабатывание связки, состоящей из обработчика и одного или нескольких каналов, возможно в силу того, что они вызываются, в типичном случае, из различных потоков.
Вторая ключевая особенность языка MC# состоит в том, что каналы и обработчики могут передаваться в качестве аргументов методам (в том числе, async- и movable- методам) отдельно от объектов, которым они принадлежат (в этом смысле, они похожи на указатели на функции в языке С, или, в терминах языка C#, на делегатов ( delegates ) ).
Третья ключевая особенность языка MC# состоит в том, что, в распределенном режиме, при копировании каналов и обработчиков на удаленную машину (под которой понимается узел кластера или некоторая машина в Grid-сети) автономно или в составе некоторого объекта, они становятся прокси-объектами, или посредниками для оригинальных каналов и обработчиков. Такая подмена скрыта от программиста - он может использовать переданные каналы и обработчики (а, в действительности, их прокси-объекты) на удаленной машине (т.е., внутри movable-методов) также, как и оригинальные: как обычно, все действия с прокси-объектами перенаправляются Runtime-системой на исходные каналы и обработчики. В этом отношении, каналы и обработчики отличаются от обычных объектов: манипуляции над последними на удаленной машине не переносятся на исходные объекты (см. первую ключевую особенность языка MC#).
Синхронизация в языке MC#
Аналогично языку Polyphonic C#, в одной связке можно определить несколько каналов. Такого вида связки являются главным средством синхронизации параллельных (в том числе, распределенных) потоков в языке MC#:
handler equals bool()& channel c1( int x ) & channel c2( int y ) { if ( x == y ) return ( true ); else return ( false ); }
Таким образом, общее правило срабатывания связки состоит в следующем: тело связки исполняется только после того, как вызваны все методы из заголовка этой связки.
При использовании связок в языке MC# нужно руководствоваться следующими правилами их корректного определения:
- Формальные параметры каналов и обработчиков не могут содержать модификаторов ref или out.
- Если в связке объявлен обработчик с типом возвращаемого значения return-type, то в теле связки должны использоваться операторы return только с выражениями, имеющими тип return-type.
- Все формальные параметры каналов и обработчика в связке должны иметь различные идентификаторы.
- Каналы и обработчики в связке не могут быть объявлены как static.
Примеры программирования на языке MC#
В этом разделе, использование специфических конструкций языка MC# будет проиллюстрировано на ряде параллельных и распределенных программ. Также излагаются и иллюстрируются общие принципы построения MC#-программ для нескольких типичных задач параллельного программирования.
Обход двоичного дерева
Если структура данных задачи организована в виде дерева, то его обработку легко распараллелить путем обработки каждого поддерева отдельном async- (movable-) методом.
Предположим, что мы имеем следующее определение (в действительности, сбалансированного) бинарного дерева в виде класса BinTree:
class BinTree { public BinTree left; public BinTree right; public int value; public BinTree( int depth ) { value = 1; if ( depth <= 1 ) { left = null; right = null; } else { left = new BinTree( depth - 1 ); right = new BinTree( depth - 1 ); } } }
Тогда просуммировать значения, находящиеся в узлах такого дерева (и, в общем случае, произвести более сложную обработку) можно с помощью следующей программы:
public class SumBinTree { public static void Main( String[] args ) { int depth = System.Convert.ToInt32( args [0] ); SumBinTree sbt = new SumBinTree(); BinTree btree = new BinTree( depth ); sbt.Sum( btree, sbt.c ); Console.WriteLine("Sum = " + sbt.Get() ); } // Определение канала и обработчика handler Get int ()& channel c( int x ) { return ( x ); } // Определение async-метода public async Sum( BinTree btree, channel (int) c ) { if ( btree.left == null ) // Дерево есть лист c ( btree.value ); else { new SumBinTree().Sum( btree.left, c1 ); new SumBinTree().Sum( btree.right, c2 ); c( Get2() ); } } // Определение связки из двух каналов и обработчика handler Get2 int()& channel с1( int x ) & channel с2( int y ) { return ( x + y ); } }
Следует также отметить, что в случае распределенного варианта этой программы, при вызове movable- метода Sum, к объекту класса BinTree, являющемуся аргументом этого метода, будут применяться процедуры сериализации/десериализации при переносе вычислений на другой компьютер. (В действительности, с точки зрения Runtime-языка MC#, поддерживающей распределенное исполнение программ, канал также является обычным объектом, к которому будут применяться процедуры сериализации/десериализации).
Вычисление частичных сумм массива
В этом разделе демонстрируется более сложный пример использования обработчиков для организации конвейера между процессами, представленными movable-методами.
Рассмотрим задачу вычисления частичных сумм массива длины
.
А именно, по заданному массиву чисел необходимо построить массив
, такой что
Идея параллельного решения этой задачи состоит в разбиении массива на
сегментов, где
кратно
, с дальнейшей одновременной обработкой этих сегментов данных длины
. Таким образом, обработка каждого сегмента будет производиться movable- методом.
(Отметим, что приведенное ниже решение пригодно и для случая, когда не кратно
. Соответствующее обобщение может рассматриваться в качестве упражнения).
Разбиение исходного массива на
сегментов производится таким образом, что в сегмент
, где (
) попадают элементы
, такие что
.
Так, например, если и
, то
0-ой сегмент составят числа
1-ый сегмент составят числа
и т.д.
Параллельный алгоритм вычисления частичных сумм будет устроен так, что -му процессу ( movable- методу), обрабатывающему
-ый сегмент данных, достаточно будет общаться лишь с его соседями слева и справа (соответственно,
-му процессу - лишь с соседом справа, а последнему,
-му процессу - лишь с соседом слева) и главной программой для возврата результатов. Процесс с номером
будет вычислять все элементы
результирующего массива, такие что
, где
.
Фрагмент главной программы, разбивающей исходный массив на сегменты и вызывающий movable- метод handleSegment, показан ниже. Здесь первым аргументом этого метода является номер сегмента, а последним - имя канала для возврата результатов.
. . . int[] segment = new int [ m ]; BDChannel[] channels = new BDChannel [ p - 1 ]; for ( i = 0; i < p; i++ ) { for ( j = 0; j < m; j++ ) segment [ j ] = f [ j * p + i ]; switch ( i ) { case 0: handleSegment( i, segment, null, channels [0], result ); break; case p-1: handleSegment(i, segment, channels [p-2], null,result); break; default: handleSegment( i, segment, channels [i-1], channels [i], result ); } }
Объекты класса BDChannel объявляются следующим образом :
class BDChannel { handler Receive object() & channel Send ( object obj ) { return ( obj ); } }
Схема взаимодействия процессов (movable-методов) между собой и главной программой показана ниже:
После разбиения, исходный массив приобретает вид двумерной матрицы, распределенной по
процессам:
Другими словами, эта матрица получена из массива разрезанием его на
сегментов и транспонированием каждого сегмента.
Ключевая идея алгоритма отдельного процесса состоит в заполнении локальных для него массивов
и
(оба, имеющие размерность
) в соответствии с формулами:
Неформально, это означает, что для процесса с номером
-ый элемент массива
есть сумма всех элементов приведенной выше матрицы, которые расположены выше и слева элемента
(включая и элементы столбца
).
Аналогично, -ый элемент массива
есть сумма всех элементов матрицы, которые расположены ниже и слева элемента
(но, не включая элементов из столбца
).
Ниже показана иллюстрация этого принципа для .
После того, как вычислены массивы и
(посредством взаимодействия с соседними процессами), процесс с номером
может вычислить элемент
результирующего массива как
Получаемые результирующие m значений процесс сохраняет в локальном массиве
для передачи их главной программе. Тогда общая схема movable -метода handleSegment выглядит следующим образом:
movable handleSegment( int number, int[] segment, BDChannel left, BDChannel right, сhannel (int[]) result ) { <Вычисление массива h0> <Вычисление массива h1> s = 0; for ( k = 1; k < m; k++ ) { h [ k ] = h0 [ k ] + s + segment [ k ] + h1 [ k ]; s = s + segment [ k ]; } h [ 0 ] = number; // Запись номера процесса-отправителя result( h ); }
Фрагмент программы, вычисляющий массив , приведен ниже.
r= 0; for ( k = 0; k < m; k++ ) { if ( left == null ) t = 0; else t = (int)left.Receive(); if ( right != null ) right.Send( t + segment [ k ] ); h0 [ k ] = r + t; r = r + t; }