|
"...Изучение и анализ примеров.
В и приведены описания и приложены исходные коды параллельных программ..." Непонятно что такое - "В и приведены описания" и где именно приведены и приложены исходные коды. |
Высокоуровневый язык параллельного программирования 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-ой сегмент составят числа ![f [ 0 ], f [ 4 ], f [ 8 ], f [ 12 ];](/sites/default/files/tex_cache/5dcdd6c71fbfef1c65ecb3d3fceb68d7.png)
1-ый сегмент составят числа ![f [ 1 ], f [ 5 ], f [ 9 ], f [ 13 ]](/sites/default/files/tex_cache/e5a24fadbda2c9f2f799c61e463747d9.png)
и т.д.
Параллельный алгоритм вычисления частичных сумм будет устроен так, что
-му процессу ( 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;
}
![h[j]=\sum_i=0^jf[i]](/sites/default/files/tex_cache/85853049fce09cb3ede604e124faa49c.png)

:



:


:


:


![h0[i]=\sum_{j=0}^{q-1}\sum_{k=0}^ia_{j,k}](/sites/default/files/tex_cache/5f68d65dda3ce68030eeb9ea7fb452fa.png)

![h1[i]=\sum_{j=q+1}^{p-1}\sum_{k=0}^{i-1}a_{j,k}](/sites/default/files/tex_cache/352a5ac5e1e824a2348edfa8de0a3dc8.png)


![h0[i]+\sum_{j=0}^ia_{q,j}+h1[i]](/sites/default/files/tex_cache/c7456c1d6f81d0054730ea0ccddbfac1.png)