Язык
Язык MC# является
Ключевая особенность языка Polyphonic C# заключается в добавлении к обычным, синхронным методам, так называемых "асинхронных" методов, которые предназначены играть в (многопоточных) программах две основные роли:
Для синхронизации нескольких асинхронных методов, а также асинхронных и синхронных методов, в язык C#, кроме того, были введены новые конструкции, получившие название связок (
При этом исполнение Polyphonic C#-программ, по замыслу авторов этого языка, по-прежнему, предполагалось либо на одной машине, либо на нескольких машинах, с зафиксированными на них асинхронными методами, взаимодействующими между собой с использованием средств удаленного вызова методов (
В случае языка MC#, программист может предусмотреть исполнение автономных асинхронных методов либо локально, либо удаленно. В последнем случае, метод может быть спланирован для исполнения на другой машине, выбираемой двумя способами: либо согласно явному указанию программиста (что не является типичным случаем), либо автоматически (обычно, на наименее загруженном узле
Таким образом, написание параллельной, распределенной программы на языке MC# сводится к выделению с помощью специального ключевого слова async методов, которые должны быть исполнены асинхронно локально (в виде отдельных потоков), а также с помощью ключевого слова movable тех методов, которые могут быть перенесены для исполнения на другие машины.
В любом традиционном языке
При исполнении программы на параллельной архитектуре, сокращение времени ее работы может быть достигнуто путем распределения множества исполняемых методов на несколько ядер одного процессора, и, возможно, отправкой части из них на другие процессоры (машины) при распределенных вычислениях.
Разделение всех методов в программе на обычные (синхронные) и асинхронные (в том числе, на те, которые могут быть перенесены для исполнения на другие машины) производится программистом с использованием специальных ключевых слов async и movable. (В языке MC#,
Async - и movable -методы являются единственным средством создания
Кроме средств создания
Основой взаимодействия
Для синхронизации
Общий синтаксис определения async - и movable -методов в языке MC# следующий:
модификаторы { async | movable } имя_метода ( аргументы )
{
< тело метода >
}
Ключевые слова async и movable располагаются на месте типа
return-type ::= type | void | async | movable
Задание ключевого слова async означает, что при вызове данного метода он будет запущен в виде отдельного потока локально, т.е., на данной машине (возможно, на отдельном ядре процессора), но без перемещения на другую машину. Ключевое слово movable означает, что данный метод при его вызове может быть спланирован для исполнения на другой машине.
Отличия async- и movable-методов от обычных методов состоят в следующем:
Соответственно, согласно правилам корректного определения async- и movable-методов:
return.Вызов movable-метода имеет две синтаксические формы:
имя_объекта.имя_метода ( аргументы )
(место исполнения метода выбирается Runtime-системой автоматически),
имя_машины@имя_объекта.имя_метода ( аргументы )
( имя_машины задает явным образом место исполнения данного метода).
При разработке распределенной программы на языке MC# (т.е., при использовании в ней movable-методов и исполнении ее на кластере или в
Во-первых, объекты, создаваемые во время исполнения MC#-программы, являются, по своей природе статическими: после своего создания, они не перемещаются и остаются привязанными к тому месту (машине), где они были созданы. В частности, именно в этом месте (на этой машине) они регистрируются Runtime-системой, что необходимо для доставки канальных сообщений этим объектам и чтения сообщений с помощью обработчиков, связанных с ними (этими объектами).
Поэтому, первой ключевой особенностью языка MC# (а точнее, его семантики) является то, что, в общем случае, во время вызова movable-метода, все необходимые данные, а именно:
только копируются (но не перемещаются) на удаленную машину. Следствием этого является то, что все изменения, которые осуществляет (прямо или косвенно) movable-метод с внутренними полями объекта, проводятся с полями объекта-копии на удаленной машине, и никак не влияют на значение полей исходного объекта.
Если копируемый (при вызове его movable-метода) объект обладает каналами или обработчиками (или же просто, они являются аргументами этого movable-метода), то они также копируются на удаленную машину. Однако, в этом случае, они становятся "прокси"-объектами для исходных каналов и обработчиков.
Каналы и обработчики канальных сообщений являются средствами для организации взаимодействия параллельных распределенных процессов между собой. Синтаксически, каналы и обработчики обычно объявляются в программе с помощью специальных конструкций - связок (
В общем случае, синтаксические правила определения связок в языке 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# состоит в том, что, в распределенном режиме, при копировании каналов и обработчиков на удаленную машину (под которой понимается узел
Аналогично языку Polyphonic C#, в одной связке можно определить несколько каналов. Такого вида связки являются главным
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# будет проиллюстрировано на ряде параллельных и распределенных программ. Также излагаются и иллюстрируются общие
Если структура данных задачи организована в виде дерева, то его обработку легко распараллелить путем обработки каждого 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, являющемуся аргументом этого метода, будут применяться процедуры
В этом разделе демонстрируется более сложный пример использования обработчиков для организации конвейера между процессами, представленными movable-методами.
Рассмотрим задачу вычисления частичных сумм массива $$f$$ длины $$n $$.
А именно, по заданному массиву чисел $$f [ 0 : n-1 ] $$ необходимо построить массив $$h [ 0 : n-1 ] $$, такой что
| $$h[j]=\sum_i=0^jf[i]$$ | для каждого $$j: 0 le; j < n $$ |
Идея параллельного решения этой задачи состоит в разбиении массива $$f $$ на $$p$$ сегментов, где $$n$$ кратно $$p$$, с дальнейшей одновременной обработкой этих сегментов данных длины $$m = n div p$$. Таким образом, обработка каждого сегмента будет производиться movable- методом.
(Отметим, что приведенное ниже решение пригодно и для случая, когда $$n$$ не кратно $$p$$. Соответствующее обобщение может рассматриваться в качестве упражнения).
Разбиение исходного массива $$f$$ на $$p$$ сегментов производится таким образом, что в сегмент $$q$$, где ( $$0 le; q < p$$ ) попадают элементы $$f [ i ] $$, такие что $$i mod p = q$$.
Так, например, если $$n = 16$$ и $$p = 4$$, то
0-ой сегмент составят числа $$f [ 0 ], f [ 4 ], f [ 8 ], f [ 12 ]; $$
1-ый сегмент составят числа $$f [ 1 ], f [ 5 ], f [ 9 ], f [ 13 ] $$
и т.д.
movable- методу), обрабатывающему $$q$$ -ый сегмент данных, достаточно будет общаться лишь с его соседями слева и справа (соответственно, $$0$$ -му процессу - лишь с соседом справа, а последнему, $$(p-1)$$ -му процессу - лишь с соседом слева) и главной программой для возврата результатов. Процесс с номером $$q$$ будет вычислять все элементы $$h [ j ] $$ результирующего массива, такие что $$j mod p = q$$, где $$0 le; j < n$$.
Фрагмент главной программы, разбивающей исходный массив на сегменты и вызывающий 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-методов) между собой и главной программой показана ниже:

После разбиения, исходный массив $$f$$ приобретает вид двумерной матрицы, распределенной по $$p$$ процессам:
| $$процесс 0$$: | $$a_{0,0}$$ | $$a_{0,1}$$ | $$\dots $$ | $$a_{0,m-1} $$ |
| $$процесс 1$$: | $$a_{1,0} $$ | $$a_{1,1} $$ | $$\dots $$ | $$a_{1,m-1} $$ |
| $$\dots $$ | $$\dots $$ | $$\dots $$ | $$\dots $$ | $$\dots $$ |
| $$процесс q$$: | $$a_{q,0} $$ | $$a_{q,1} $$ | $$\dots $$ | $$a_{q,m-1} $$ |
| $$\dots $$ | $$\dots $$ | $$\dots $$ | $$\dots $$ | $$\dots $$ |
| $$процесс p-1$$: | $$a_{p-1,0} $$ | $$a_{p-1,1} $$ | $$\dots $$ | $$a_{p-1,m-1} $$ |
Другими словами, эта матрица получена из массива $$f $$ разрезанием его на $$p $$ сегментов и
Ключевая идея алгоритма отдельного процесса $$q $$ состоит в заполнении локальных для него массивов $$h0 $$ и $$h1 $$ (оба, имеющие размерность $$m $$ ) в соответствии с формулами:
| $$h0[i]=\sum_{j=0}^{q-1}\sum_{k=0}^ia_{j,k} $$ | $$0 le; i < m$$ |
| $$h1[i]=\sum_{j=q+1}^{p-1}\sum_{k=0}^{i-1}a_{j,k}$$ | $$0le;i<m$$ |
Неформально, это означает, что для процесса с номером $$q$$ $$i$$ -ый элемент массива $$h0$$ есть сумма всех элементов приведенной выше матрицы, которые расположены выше и слева элемента $$a_{q,i} $$ (включая и элементы столбца $$i$$ ).
Аналогично, $$i$$ -ый элемент массива $$h1$$ есть сумма всех элементов матрицы, которые расположены ниже и слева элемента $$a_{q,i}$$ (но, не включая элементов из столбца $$i$$ ).
Ниже показана иллюстрация этого принципа для $$n = 16, p = 4 и q = 1, i = 2$$.

После того, как вычислены массивы $$h0$$ и $$h1$$ (посредством взаимодействия с соседними процессами), процесс с номером $$q$$ может вычислить элемент $$h[ i * p + q ] $$ результирующего массива как
| $$h0[i]+\sum_{j=0}^ia_{q,j}+h1[i] $$ | для всех $$i: 0 le; i < m $$ |
Получаемые результирующие m значений процесс $$q $$ сохраняет в локальном массиве $$h $$ для передачи их главной программе. Тогда общая схема 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 );
}
Фрагмент программы, вычисляющий массив $$h0 $$, приведен ниже.
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;
}
Хорошо известной задачей в учебниках по элементарному программированию, структурам данных и алгоритмам, является задача о расстановке на шахматной доске восьми ферзей таким образом, чтобы ни один из них не находился под боем какого-либо другого из ферзей. То, что эта задача имеет решение, было продемонстрировано Карлом Фридрихом Гауссом и Францем Науком в 1850 году. На самом деле, имеется 92 различных способа расставить указанным образом ферзей на обычной шахматной доске.
Вычисление количества решений и всех их перечисление для данной задачи является одной из базовых проблем компьютерного программирования. Задача о 8 ферзях естественным образом обобщается до задачи об $$N $$ > ферзях, когда задается число $$N $$ - размер шахматной доски, и требуется найти все способы расстановки $$N $$ ферзей на этой доске, чтобы они попарно не атаковали друг друга.
Для решения этой задачи предлагались различные методы - некоторые из них можно найти в известной книге Н. Вирта "Алгоритмы + Структуры Данных = Программы". Далее будут рассмотрены теоретические основы и реализация
Далее, при объяснении
Прямая проверка. Предположим, что ферзь находится на $$i $$ -ой горизонтали (строке) и на $$j $$ -ой вертикали (столбце), т.е., на клетке ( $$i,j $$ ). Тогда, клетки, которые находятся под боем данного ферзя, включают в себя: все клетки $$i $$ -ой строки и все клетки $$j $$ -го столбца, а также все клетки ( $$k,l $$ ), где $$k -l = i - j $$, или $$k + l = i + j $$. Последние две группы клеток располагаются на двух диагоналях, которые находятся под боем ферзя, расположенного в клетке ( $$i,j $$ ).
Если на доске уже расположены безопасным образом несколько ферзей и их число меньше, чем 8, то поиск позиции для очередного ферзя сводится к проверке пустых клеток и проверке для них вышеупомянутых условий относительно каждого из ферзей, уже находящихся на доске. Если ни одна из таких клеток не подходит, то мы должны снять с доски последнего из поставленных ферзей и попробовать для него другие возможности.
Эта процедура может быть представлена алгоритмически в виде последовательности проверок, при которых вначале делается попытка выставить ферзя в первой строке, затем второго ферзя - во второй строке, и т.д. Если для очередного ферзя в $$i $$ -ой строке безопасная позиция отсутствует, то необходима процедура бектрекинга.
Метод на основе контрольных массивов. Недостатком метода прямых проверок являются повторные вычисления проверок для каждого из ферзей, находящихся на доске. Эти повторные вычисления могут быть устранены.
В усовершенствованном методе используются три массива и >right для хранения информации о текущей конфигурации ферзей, которые состоят из 8, 15 и 15 элементов соответственно (так как $$15 = 8 + ( 8 - 1 ) $$ при $$N = 8 $$ ). Пусть равно 1, если имеется ферзь в $$i $$ -ом столбце доски, и $$0 $$ - в противном случае. Если ферзь находится в позиции ( $$i,j $$ ), то $$left [ i + j ] $$ и $$right [ 7 - j + i ] $$ будут равны 1, в противном случае - 0 (как обычно, мы предполагаем, что нумерация элементов массива начинается с 0).
Имея такие массивы, проверка очередной клетки на возможность размещения в ней очередного ферзя, становится прямой и эффективной. Для проверки позиции ( $$i^',j^' $$ ), нам необходимо только проверить элементы и right[7-j^'+i^']. Если все три элемента массивов равны 0, то позиция ( $$i^',j^' $$ ) является безопасной для нового ферзя. Для поиска безопасной расстановки или всех возможных таких расстановок, как обычно, используются процедуры
Следующий шаг в повышении
В следующем разделе приводится решение, которое для работы с битовыми векторами использует только
Предположим, что $$b_1 $$ является
Также легко найти позиции, контролируемые первым ферзем в третьей строке: достаточно сдвинуть вектор $$b_1$$ влево или вправо еще на один бит. Т.е.:
$$b_1 | ( b_1 < < 2) | (b_1 > > 2)$$В общем случае, позиции, контролируемые первым ферзем в $$k$$ -ой строке, могут быть определены путем сдвига вектора $$b_1$$ на $$k - 1$$ битов влево и вправо и выполнением побитовой операции ИЛИ над этими тремя векторами. Ферзь в первой строке контролирует в каждой строке ниже самое большее три позиции. Отметим также, что при сдвиге биты, содержащие 1, могут выходить за пределы векторов, размер которых совпадает с размером доски. Пусть $$B_i[k] $$ обозначает вектор, представляющий контролируемые позиции в строке $$k$$ ферзем, находящимся в строке $$i ( k > i ) $$. Тогда, очевидно, имеем:
$$B_i[k] = b_i | ( b_i < < ( k - i ) ) | (b_i > > ( k - i ) )$$Существуют различные способы представления задачи о расстановке ферзей на доске 8 х 8 с помощью
Однако, существует естественный способ объединить все эти управляющие векторы в три рабочих вектора, которые будут называться left, down и right, соответственно.
Вектор down представляет столбцы, которые на текущий момент контролируются уже выставленными ферзями. Когда новый ферзь ставится на доску, соответствующий бит вектора down принимает значение 1.
Вектор left представляет контролируемые выставленными ферзями позиции влево от них по диагонали. На каждом шаге, при переходе к следующей строке вектор left сдвигается на один бит влево. Когда новый ферзь ставится на доску, в векторе left соответствующий бит должен получить значение 1. Вектор right играет ту же самую роль, что и вектор left, но только для направления вправо по диагонали.
Такое представление контролируемых позиций достаточно хорошо вписывается в общую процедуру поиска с использованием бектрекинга. Выполнение поиска начинается с первой строки при значениях всех векторов равных нулю. В каждой строке, контролируемые позиции определяются выражением
$$B = left | down | right$$На каждом шаге, новый ферзь добавляется в некоторую позицию, для которой бит в векторе $$B$$ равен 0, и все три рабочих вектора модифицируются в соответствующем бите. После этого, происходит переход к следующему шагу, при котором векторы left и right сдвигаются на 1 бит влево и вправо, соответственно. Тем самым, для каждой строки поддерживается
контролируемые позиции = left | down | right,
который гарантирует корректность процедуры.
Описанный процесс поиска с бектрекингом легко реализовать в виде (последовательной) рекурсивной процедуры на языке C#:
using System;
public class NQueens {
public static int N, MASK, COUNT;
public static void Backtrack(int y, int left, int down, int right)
{
int bitmap, bit;
if (y == N) {
COUNT++;
} else {
bitmap = MASK ~(left | down | right);
while (bitmap != 0) {
bit = -bitmap bitmap;
bitmap ^= bit;
Backtrack(y+1, (left | bit) < <1, down | bit, (right | bit) > >1);
}
}
}
public static void Main(String[] args )
{
N = 10; /* <- N */
COUNT = 0; /* result */
MASK = (1 < < N) - 1;
Backtrack(0, 0, 0, 0);
Console.WriteLine ("N=" + N + " -> " + COUNT);
return;
}
}
Базовая идея
Однако, прямой вариант легко обобщить путем независимого поиска решений для всех допустимых конфигураций ферзей на первых $$M ( M le; N ) $$ строках. Очевидно, что число таких конфигураций не превышает $$N^M$$, и все они могут быть сгенерированы с помощью процедуры , приведенной выше.
Все эти конфигурации оформляются в виде объектов класса Task, которые в качестве своих полей содержат векторы left, down и right, представляющие очередную расстановку ферзей на первых $$M$$ строках, для которой будет искаться полная расстановка.
Таким образом, отдельные процессоры ( ы), будут брать из некоторой очереди (представляемой каналом sendTask и обработчиком getTask ) очередную задачу, решать ее и пересылать ответ главной программе при запросе очередного задания. заканчивает свою работу при считывании из очереди концевого маркера (объекта класса Task со значением -1 для каждого из векторов left, down,right ).
Полный текст программы NQueens на языке MC# представлен ниже.
using System;
public class Task {
public int left, down, right;
public Task ( int l, int d, int r ) {
left = l;
down = d;
right = r;
}
}
//**************************************************//
public class NQueens {
public static long totalCount = 0;
public static void Main ( String[] args ) {
int N = System.Convert.ToInt32 ( args [ 0 ] ); // Board size
int M = System.Convert.ToInt32 ( args [ 1 ] ); // Number of fixed queens
int P = System.Convert.ToInt32 ( args [ 2 ] ); // Number of workers
NQueens nqueens = new NQueens();
nqueens.launchWorkers ( N, M, P, nqueens.getTask, nqueens.sendStop, nqueens );
nqueens.generateTasks ( N, M, P, nqueens.sendTask );
for ( int i = 0; i < P; i++ )
nqueens.getStop ? ();
Console.Write ("Task challenge : " + N + " " );
Console.WriteLine ("Solutions = " + totalCount );
}
//***************************************************************************//
public handler getTask Task(int count ) channel sendTask ( Task task ) {
totalCount += count;
return ( task );
}
//***************************************************************************//
public handler getStop void() channel sendStop () {
return;
}
//***************************************************************************//
public async launchWorkers ( int N, int M, int P, handler Task(int) getTask,
channel () sendStop, NQueens nqueens ){
for ( int i = 0; i < P; i++ )
nqueens.Worker ( i, N, M, getTask, sendStop );
}
//***************************************************************************//
public void generateTasks ( int N, int M, int P, channel (Task) sendTask ) {
int y = 0;
int left = 0;
int down = 0;
int right = 0;
int MASK = ( 1 < < N ) - 1;
MainBacktrack ( y, left, down, right, MASK, M, sendTask );
Task finish_marker = new Task ( -1, -1, -1 );
for ( int i = 0; i < P; i++ )
sendTask ! ( finish_marker );
}
//***************************************************************************//
public void MainBacktrack ( int y, int left, int down, int right, int MASK,
int M, channel (Task) sendTask ) {
int bitmap, bit;
if ( y == M )
sendTask ! ( new Task ( left, down, right ) );
else {
bitmap = MASK ~ ( left | down | right );
while ( bitmap != 0 ) {
bit = -bitmap bitmap;
bitmap = bitmap ^ bit;
MainBacktrack (y + 1, (left | bit) < <1, down | bit, ( right | bit ) > > > 1,
MASK, M, sendTask );
}
}
}
//***************************************************************************//
public async Worker ( int myNumber, int N, int M, handler Task(int) getTask,
channel () sendStop ) {
int MASK = ( 1 < < N ) - 1;
int count = 0;
Task task = (Task) getTask ? ( count );
while ( task.left != -1 ) {
WorkerBacktrack ( M, task.left, task.down, task.right, MASK, N, ref count );
task = (Task) getTask ? ( count );
count = 0;
}
sendStop ! ();
}
//***************************************************************************//
public void WorkerBacktrack ( int y, int left, int down, int right, int MASK,
int N, ref int count ) {
int bitmap, bit;
if ( y == N )
count++;
else {
bitmap = MASK ~ ( left | down | right );
while ( bitmap != 0 ) {
bit = -bitmap bitmap;
bitmap = bitmap ^ bit;
WorkerBacktrack ( y + 1, (left|bit) < < 1, down|bit, (right|bit) > > 1,
MASK, N, ref count );
}
}
}
}
NQueens, использующий movable- методы.Как обычно, для любого параллельного языка программирования, реализация MC# состоит из компилятора и рантайм-системы. Главными функциональными частями рантайм-системы являются:
Компилятор переводит программу из MC# в C#, его главной целью является создание кода, реализующего: выполнение movable-методов на других процессорах; пересылку канальных сообщений и; синхронизацию методов, объединенных связкой. Эти функции предоставляются соответствующими методами классов рантайм-системы. Среди них:
Главные функции компилятора MC#:
Init() и Finalize() класса Session в главном методе программы. Функция Init() доставляет исполняемый модуль программы на другие узлы, Finalize() останавливает запущенные потоки и завершает вычислительную сессию.Язык
Язык MC# является
Ключевая особенность языка Polyphonic C# заключается в добавлении к обычным, синхронным методам, так называемых "асинхронных" методов, которые предназначены играть в (многопоточных) программах две основные роли:
Для синхронизации нескольких асинхронных методов, а также асинхронных и синхронных методов, в язык C#, кроме того, были введены новые конструкции, получившие название связок (
При этом исполнение Polyphonic C#-программ, по замыслу авторов этого языка, по-прежнему, предполагалось либо на одной машине, либо на нескольких машинах, с зафиксированными на них асинхронными методами, взаимодействующими между собой с использованием средств удаленного вызова методов (
В случае языка MC#, программист может предусмотреть исполнение автономных асинхронных методов либо локально, либо удаленно. В последнем случае, метод может быть спланирован для исполнения на другой машине, выбираемой двумя способами: либо согласно явному указанию программиста (что не является типичным случаем), либо автоматически (обычно, на наименее загруженном узле
Таким образом, написание параллельной, распределенной программы на языке MC# сводится к выделению с помощью специального ключевого слова async методов, которые должны быть исполнены асинхронно локально (в виде отдельных потоков), а также с помощью ключевого слова movable тех методов, которые могут быть перенесены для исполнения на другие машины.
В любом традиционном языке
При исполнении программы на параллельной архитектуре, сокращение времени ее работы может быть достигнуто путем распределения множества исполняемых методов на несколько ядер одного процессора, и, возможно, отправкой части из них на другие процессоры (машины) при распределенных вычислениях.
Разделение всех методов в программе на обычные (синхронные) и асинхронные (в том числе, на те, которые могут быть перенесены для исполнения на другие машины) производится программистом с использованием специальных ключевых слов async и movable. (В языке MC#,
Async - и movable -методы являются единственным средством создания
Кроме средств создания
Основой взаимодействия
Для синхронизации
Общий синтаксис определения async - и movable -методов в языке MC# следующий:
модификаторы { async | movable } имя_метода ( аргументы )
{
< тело метода >
}
Ключевые слова async и movable располагаются на месте типа
return-type ::= type | void | async | movable
Задание ключевого слова async означает, что при вызове данного метода он будет запущен в виде отдельного потока локально, т.е., на данной машине (возможно, на отдельном ядре процессора), но без перемещения на другую машину. Ключевое слово movable означает, что данный метод при его вызове может быть спланирован для исполнения на другой машине.
Отличия async- и movable-методов от обычных методов состоят в следующем:
Соответственно, согласно правилам корректного определения async- и movable-методов:
return.Вызов movable-метода имеет две синтаксические формы:
имя_объекта.имя_метода ( аргументы )
(место исполнения метода выбирается Runtime-системой автоматически),
имя_машины@имя_объекта.имя_метода ( аргументы )
( имя_машины задает явным образом место исполнения данного метода).
При разработке распределенной программы на языке MC# (т.е., при использовании в ней movable-методов и исполнении ее на кластере или в
Во-первых, объекты, создаваемые во время исполнения MC#-программы, являются, по своей природе статическими: после своего создания, они не перемещаются и остаются привязанными к тому месту (машине), где они были созданы. В частности, именно в этом месте (на этой машине) они регистрируются Runtime-системой, что необходимо для доставки канальных сообщений этим объектам и чтения сообщений с помощью обработчиков, связанных с ними (этими объектами).
Поэтому, первой ключевой особенностью языка MC# (а точнее, его семантики) является то, что, в общем случае, во время вызова movable-метода, все необходимые данные, а именно:
только копируются (но не перемещаются) на удаленную машину. Следствием этого является то, что все изменения, которые осуществляет (прямо или косвенно) movable-метод с внутренними полями объекта, проводятся с полями объекта-копии на удаленной машине, и никак не влияют на значение полей исходного объекта.
Если копируемый (при вызове его movable-метода) объект обладает каналами или обработчиками (или же просто, они являются аргументами этого movable-метода), то они также копируются на удаленную машину. Однако, в этом случае, они становятся "прокси"-объектами для исходных каналов и обработчиков.
Каналы и обработчики канальных сообщений являются средствами для организации взаимодействия параллельных распределенных процессов между собой. Синтаксически, каналы и обработчики обычно объявляются в программе с помощью специальных конструкций - связок (
В общем случае, синтаксические правила определения связок в языке 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# состоит в том, что, в распределенном режиме, при копировании каналов и обработчиков на удаленную машину (под которой понимается узел
Аналогично языку Polyphonic C#, в одной связке можно определить несколько каналов. Такого вида связки являются главным
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# будет проиллюстрировано на ряде параллельных и распределенных программ. Также излагаются и иллюстрируются общие
Если структура данных задачи организована в виде дерева, то его обработку легко распараллелить путем обработки каждого 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, являющемуся аргументом этого метода, будут применяться процедуры
В этом разделе демонстрируется более сложный пример использования обработчиков для организации конвейера между процессами, представленными movable-методами.
Рассмотрим задачу вычисления частичных сумм массива $$f$$ длины $$n $$.
А именно, по заданному массиву чисел $$f [ 0 : n-1 ] $$ необходимо построить массив $$h [ 0 : n-1 ] $$, такой что
| $$h[j]=\sum_i=0^jf[i]$$ | для каждого $$j: 0 le; j < n $$ |
Идея параллельного решения этой задачи состоит в разбиении массива $$f $$ на $$p$$ сегментов, где $$n$$ кратно $$p$$, с дальнейшей одновременной обработкой этих сегментов данных длины $$m = n div p$$. Таким образом, обработка каждого сегмента будет производиться movable- методом.
(Отметим, что приведенное ниже решение пригодно и для случая, когда $$n$$ не кратно $$p$$. Соответствующее обобщение может рассматриваться в качестве упражнения).
Разбиение исходного массива $$f$$ на $$p$$ сегментов производится таким образом, что в сегмент $$q$$, где ( $$0 le; q < p$$ ) попадают элементы $$f [ i ] $$, такие что $$i mod p = q$$.
Так, например, если $$n = 16$$ и $$p = 4$$, то
0-ой сегмент составят числа $$f [ 0 ], f [ 4 ], f [ 8 ], f [ 12 ]; $$
1-ый сегмент составят числа $$f [ 1 ], f [ 5 ], f [ 9 ], f [ 13 ] $$
и т.д.
movable- методу), обрабатывающему $$q$$ -ый сегмент данных, достаточно будет общаться лишь с его соседями слева и справа (соответственно, $$0$$ -му процессу - лишь с соседом справа, а последнему, $$(p-1)$$ -му процессу - лишь с соседом слева) и главной программой для возврата результатов. Процесс с номером $$q$$ будет вычислять все элементы $$h [ j ] $$ результирующего массива, такие что $$j mod p = q$$, где $$0 le; j < n$$.
Фрагмент главной программы, разбивающей исходный массив на сегменты и вызывающий 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-методов) между собой и главной программой показана ниже:

После разбиения, исходный массив $$f$$ приобретает вид двумерной матрицы, распределенной по $$p$$ процессам:
| $$процесс 0$$: | $$a_{0,0}$$ | $$a_{0,1}$$ | $$\dots $$ | $$a_{0,m-1} $$ |
| $$процесс 1$$: | $$a_{1,0} $$ | $$a_{1,1} $$ | $$\dots $$ | $$a_{1,m-1} $$ |
| $$\dots $$ | $$\dots $$ | $$\dots $$ | $$\dots $$ | $$\dots $$ |
| $$процесс q$$: | $$a_{q,0} $$ | $$a_{q,1} $$ | $$\dots $$ | $$a_{q,m-1} $$ |
| $$\dots $$ | $$\dots $$ | $$\dots $$ | $$\dots $$ | $$\dots $$ |
| $$процесс p-1$$: | $$a_{p-1,0} $$ | $$a_{p-1,1} $$ | $$\dots $$ | $$a_{p-1,m-1} $$ |
Другими словами, эта матрица получена из массива $$f $$ разрезанием его на $$p $$ сегментов и
Ключевая идея алгоритма отдельного процесса $$q $$ состоит в заполнении локальных для него массивов $$h0 $$ и $$h1 $$ (оба, имеющие размерность $$m $$ ) в соответствии с формулами:
| $$h0[i]=\sum_{j=0}^{q-1}\sum_{k=0}^ia_{j,k} $$ | $$0 le; i < m$$ |
| $$h1[i]=\sum_{j=q+1}^{p-1}\sum_{k=0}^{i-1}a_{j,k}$$ | $$0le;i<m$$ |
Неформально, это означает, что для процесса с номером $$q$$ $$i$$ -ый элемент массива $$h0$$ есть сумма всех элементов приведенной выше матрицы, которые расположены выше и слева элемента $$a_{q,i} $$ (включая и элементы столбца $$i$$ ).
Аналогично, $$i$$ -ый элемент массива $$h1$$ есть сумма всех элементов матрицы, которые расположены ниже и слева элемента $$a_{q,i}$$ (но, не включая элементов из столбца $$i$$ ).
Ниже показана иллюстрация этого принципа для $$n = 16, p = 4 и q = 1, i = 2$$.

После того, как вычислены массивы $$h0$$ и $$h1$$ (посредством взаимодействия с соседними процессами), процесс с номером $$q$$ может вычислить элемент $$h[ i * p + q ] $$ результирующего массива как
| $$h0[i]+\sum_{j=0}^ia_{q,j}+h1[i] $$ | для всех $$i: 0 le; i < m $$ |
Получаемые результирующие m значений процесс $$q $$ сохраняет в локальном массиве $$h $$ для передачи их главной программе. Тогда общая схема 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 );
}
Фрагмент программы, вычисляющий массив $$h0 $$, приведен ниже.
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;
}
Хорошо известной задачей в учебниках по элементарному программированию, структурам данных и алгоритмам, является задача о расстановке на шахматной доске восьми ферзей таким образом, чтобы ни один из них не находился под боем какого-либо другого из ферзей. То, что эта задача имеет решение, было продемонстрировано Карлом Фридрихом Гауссом и Францем Науком в 1850 году. На самом деле, имеется 92 различных способа расставить указанным образом ферзей на обычной шахматной доске.
Вычисление количества решений и всех их перечисление для данной задачи является одной из базовых проблем компьютерного программирования. Задача о 8 ферзях естественным образом обобщается до задачи об $$N $$ > ферзях, когда задается число $$N $$ - размер шахматной доски, и требуется найти все способы расстановки $$N $$ ферзей на этой доске, чтобы они попарно не атаковали друг друга.
Для решения этой задачи предлагались различные методы - некоторые из них можно найти в известной книге Н. Вирта "Алгоритмы + Структуры Данных = Программы". Далее будут рассмотрены теоретические основы и реализация
Далее, при объяснении
Прямая проверка. Предположим, что ферзь находится на $$i $$ -ой горизонтали (строке) и на $$j $$ -ой вертикали (столбце), т.е., на клетке ( $$i,j $$ ). Тогда, клетки, которые находятся под боем данного ферзя, включают в себя: все клетки $$i $$ -ой строки и все клетки $$j $$ -го столбца, а также все клетки ( $$k,l $$ ), где $$k -l = i - j $$, или $$k + l = i + j $$. Последние две группы клеток располагаются на двух диагоналях, которые находятся под боем ферзя, расположенного в клетке ( $$i,j $$ ).
Если на доске уже расположены безопасным образом несколько ферзей и их число меньше, чем 8, то поиск позиции для очередного ферзя сводится к проверке пустых клеток и проверке для них вышеупомянутых условий относительно каждого из ферзей, уже находящихся на доске. Если ни одна из таких клеток не подходит, то мы должны снять с доски последнего из поставленных ферзей и попробовать для него другие возможности.
Эта процедура может быть представлена алгоритмически в виде последовательности проверок, при которых вначале делается попытка выставить ферзя в первой строке, затем второго ферзя - во второй строке, и т.д. Если для очередного ферзя в $$i $$ -ой строке безопасная позиция отсутствует, то необходима процедура бектрекинга.
Метод на основе контрольных массивов. Недостатком метода прямых проверок являются повторные вычисления проверок для каждого из ферзей, находящихся на доске. Эти повторные вычисления могут быть устранены.
В усовершенствованном методе используются три массива и >right для хранения информации о текущей конфигурации ферзей, которые состоят из 8, 15 и 15 элементов соответственно (так как $$15 = 8 + ( 8 - 1 ) $$ при $$N = 8 $$ ). Пусть равно 1, если имеется ферзь в $$i $$ -ом столбце доски, и $$0 $$ - в противном случае. Если ферзь находится в позиции ( $$i,j $$ ), то $$left [ i + j ] $$ и $$right [ 7 - j + i ] $$ будут равны 1, в противном случае - 0 (как обычно, мы предполагаем, что нумерация элементов массива начинается с 0).
Имея такие массивы, проверка очередной клетки на возможность размещения в ней очередного ферзя, становится прямой и эффективной. Для проверки позиции ( $$i^',j^' $$ ), нам необходимо только проверить элементы и right[7-j^'+i^']. Если все три элемента массивов равны 0, то позиция ( $$i^',j^' $$ ) является безопасной для нового ферзя. Для поиска безопасной расстановки или всех возможных таких расстановок, как обычно, используются процедуры
Следующий шаг в повышении
В следующем разделе приводится решение, которое для работы с битовыми векторами использует только
Предположим, что $$b_1 $$ является
Также легко найти позиции, контролируемые первым ферзем в третьей строке: достаточно сдвинуть вектор $$b_1$$ влево или вправо еще на один бит. Т.е.:
$$b_1 | ( b_1 < < 2) | (b_1 > > 2)$$В общем случае, позиции, контролируемые первым ферзем в $$k$$ -ой строке, могут быть определены путем сдвига вектора $$b_1$$ на $$k - 1$$ битов влево и вправо и выполнением побитовой операции ИЛИ над этими тремя векторами. Ферзь в первой строке контролирует в каждой строке ниже самое большее три позиции. Отметим также, что при сдвиге биты, содержащие 1, могут выходить за пределы векторов, размер которых совпадает с размером доски. Пусть $$B_i[k] $$ обозначает вектор, представляющий контролируемые позиции в строке $$k$$ ферзем, находящимся в строке $$i ( k > i ) $$. Тогда, очевидно, имеем:
$$B_i[k] = b_i | ( b_i < < ( k - i ) ) | (b_i > > ( k - i ) )$$Существуют различные способы представления задачи о расстановке ферзей на доске 8 х 8 с помощью
Однако, существует естественный способ объединить все эти управляющие векторы в три рабочих вектора, которые будут называться left, down и right, соответственно.
Вектор down представляет столбцы, которые на текущий момент контролируются уже выставленными ферзями. Когда новый ферзь ставится на доску, соответствующий бит вектора down принимает значение 1.
Вектор left представляет контролируемые выставленными ферзями позиции влево от них по диагонали. На каждом шаге, при переходе к следующей строке вектор left сдвигается на один бит влево. Когда новый ферзь ставится на доску, в векторе left соответствующий бит должен получить значение 1. Вектор right играет ту же самую роль, что и вектор left, но только для направления вправо по диагонали.
Такое представление контролируемых позиций достаточно хорошо вписывается в общую процедуру поиска с использованием бектрекинга. Выполнение поиска начинается с первой строки при значениях всех векторов равных нулю. В каждой строке, контролируемые позиции определяются выражением
$$B = left | down | right$$На каждом шаге, новый ферзь добавляется в некоторую позицию, для которой бит в векторе $$B$$ равен 0, и все три рабочих вектора модифицируются в соответствующем бите. После этого, происходит переход к следующему шагу, при котором векторы left и right сдвигаются на 1 бит влево и вправо, соответственно. Тем самым, для каждой строки поддерживается
контролируемые позиции = left | down | right,
который гарантирует корректность процедуры.
Описанный процесс поиска с бектрекингом легко реализовать в виде (последовательной) рекурсивной процедуры на языке C#:
using System;
public class NQueens {
public static int N, MASK, COUNT;
public static void Backtrack(int y, int left, int down, int right)
{
int bitmap, bit;
if (y == N) {
COUNT++;
} else {
bitmap = MASK ~(left | down | right);
while (bitmap != 0) {
bit = -bitmap bitmap;
bitmap ^= bit;
Backtrack(y+1, (left | bit) < <1, down | bit, (right | bit) > >1);
}
}
}
public static void Main(String[] args )
{
N = 10; /* <- N */
COUNT = 0; /* result */
MASK = (1 < < N) - 1;
Backtrack(0, 0, 0, 0);
Console.WriteLine ("N=" + N + " -> " + COUNT);
return;
}
}
Базовая идея
Однако, прямой вариант легко обобщить путем независимого поиска решений для всех допустимых конфигураций ферзей на первых $$M ( M le; N ) $$ строках. Очевидно, что число таких конфигураций не превышает $$N^M$$, и все они могут быть сгенерированы с помощью процедуры , приведенной выше.
Все эти конфигурации оформляются в виде объектов класса Task, которые в качестве своих полей содержат векторы left, down и right, представляющие очередную расстановку ферзей на первых $$M$$ строках, для которой будет искаться полная расстановка.
Таким образом, отдельные процессоры ( ы), будут брать из некоторой очереди (представляемой каналом sendTask и обработчиком getTask ) очередную задачу, решать ее и пересылать ответ главной программе при запросе очередного задания. заканчивает свою работу при считывании из очереди концевого маркера (объекта класса Task со значением -1 для каждого из векторов left, down,right ).
Полный текст программы NQueens на языке MC# представлен ниже.
using System;
public class Task {
public int left, down, right;
public Task ( int l, int d, int r ) {
left = l;
down = d;
right = r;
}
}
//**************************************************//
public class NQueens {
public static long totalCount = 0;
public static void Main ( String[] args ) {
int N = System.Convert.ToInt32 ( args [ 0 ] ); // Board size
int M = System.Convert.ToInt32 ( args [ 1 ] ); // Number of fixed queens
int P = System.Convert.ToInt32 ( args [ 2 ] ); // Number of workers
NQueens nqueens = new NQueens();
nqueens.launchWorkers ( N, M, P, nqueens.getTask, nqueens.sendStop, nqueens );
nqueens.generateTasks ( N, M, P, nqueens.sendTask );
for ( int i = 0; i < P; i++ )
nqueens.getStop ? ();
Console.Write ("Task challenge : " + N + " " );
Console.WriteLine ("Solutions = " + totalCount );
}
//***************************************************************************//
public handler getTask Task(int count ) channel sendTask ( Task task ) {
totalCount += count;
return ( task );
}
//***************************************************************************//
public handler getStop void() channel sendStop () {
return;
}
//***************************************************************************//
public async launchWorkers ( int N, int M, int P, handler Task(int) getTask,
channel () sendStop, NQueens nqueens ){
for ( int i = 0; i < P; i++ )
nqueens.Worker ( i, N, M, getTask, sendStop );
}
//***************************************************************************//
public void generateTasks ( int N, int M, int P, channel (Task) sendTask ) {
int y = 0;
int left = 0;
int down = 0;
int right = 0;
int MASK = ( 1 < < N ) - 1;
MainBacktrack ( y, left, down, right, MASK, M, sendTask );
Task finish_marker = new Task ( -1, -1, -1 );
for ( int i = 0; i < P; i++ )
sendTask ! ( finish_marker );
}
//***************************************************************************//
public void MainBacktrack ( int y, int left, int down, int right, int MASK,
int M, channel (Task) sendTask ) {
int bitmap, bit;
if ( y == M )
sendTask ! ( new Task ( left, down, right ) );
else {
bitmap = MASK ~ ( left | down | right );
while ( bitmap != 0 ) {
bit = -bitmap bitmap;
bitmap = bitmap ^ bit;
MainBacktrack (y + 1, (left | bit) < <1, down | bit, ( right | bit ) > > > 1,
MASK, M, sendTask );
}
}
}
//***************************************************************************//
public async Worker ( int myNumber, int N, int M, handler Task(int) getTask,
channel () sendStop ) {
int MASK = ( 1 < < N ) - 1;
int count = 0;
Task task = (Task) getTask ? ( count );
while ( task.left != -1 ) {
WorkerBacktrack ( M, task.left, task.down, task.right, MASK, N, ref count );
task = (Task) getTask ? ( count );
count = 0;
}
sendStop ! ();
}
//***************************************************************************//
public void WorkerBacktrack ( int y, int left, int down, int right, int MASK,
int N, ref int count ) {
int bitmap, bit;
if ( y == N )
count++;
else {
bitmap = MASK ~ ( left | down | right );
while ( bitmap != 0 ) {
bit = -bitmap bitmap;
bitmap = bitmap ^ bit;
WorkerBacktrack ( y + 1, (left|bit) < < 1, down|bit, (right|bit) > > 1,
MASK, N, ref count );
}
}
}
}
NQueens, использующий movable- методы.Как обычно, для любого параллельного языка программирования, реализация MC# состоит из компилятора и рантайм-системы. Главными функциональными частями рантайм-системы являются:
Компилятор переводит программу из MC# в C#, его главной целью является создание кода, реализующего: выполнение movable-методов на других процессорах; пересылку канальных сообщений и; синхронизацию методов, объединенных связкой. Эти функции предоставляются соответствующими методами классов рантайм-системы. Среди них:
Главные функции компилятора MC#:
Init() и Finalize() класса Session в главном методе программы. Функция Init() доставляет исполняемый модуль программы на другие узлы, Finalize() останавливает запущенные потоки и завершает вычислительную сессию.Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.