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

Высокоуровневый язык параллельного программирования MC#

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

Язык параллельного программирования MC# предназначен для написания программ, работающих на всём спектре параллельных архитектур - от многоядерных процессоров до Grid-сетей. Единственное требование к таким системам со стороны MC# - на них должна быть установлена среда исполнения CLR (Common Language Runtime) с соответствующим набором библиотек. На машинах с операционной системой Windows реализацией такой среды является Microsoft .NET Framework, а на машинах с операционной системой Linux - система Mono (http://www.mono-project.com), которая является свободной реализацией платформы .NET для Unix-подобных систем.

Язык MC# является адаптацией и развитием базовых идей языка Polyphonic C# на случай параллельных и распределенных вычислений. Язык Polyphonic C# был разработан в 2002г. в Microsoft Research Laboratory (г. Кембридж, Великобритания) Н. Бентоном (N. Benton), Л. Карделли (L. Cardelli) и Ц. Фурнье (C. Fournet). Целью его создания было добавление высокоуровневых средств асинхронного параллельного программирования в язык C# для использования в серверных и клиент-серверных приложениях на базе Microsoft .NET Framework.

Ключевая особенность языка Polyphonic C# заключается в добавлении к обычным, синхронным методам, так называемых "асинхронных" методов, которые предназначены играть в (многопоточных) программах две основные роли:

  • автономных методов, предназначенных для выполнения базовой вычислительной работы, и исполняемых в отдельных потоках, и
  • методов, предназначенных для доставки данных (сигналов) обычным, синхронным методам.
  • Для синхронизации нескольких асинхронных методов, а также асинхронных и синхронных методов, в язык C#, кроме того, были введены новые конструкции, получившие название связок (chords).

    При этом исполнение Polyphonic C#-программ, по замыслу авторов этого языка, по-прежнему, предполагалось либо на одной машине, либо на нескольких машинах, с зафиксированными на них асинхронными методами, взаимодействующими между собой с использованием средств удаленного вызова методов (RMI - Remote Method Invocation), предоставляемых библиотекой System.Runtime.Remoting платформы .NET.

    В случае языка MC#, программист может предусмотреть исполнение автономных асинхронных методов либо локально, либо удаленно. В последнем случае, метод может быть спланирован для исполнения на другой машине, выбираемой двумя способами: либо согласно явному указанию программиста (что не является типичным случаем), либо автоматически (обычно, на наименее загруженном узле кластера или машине Grid-сети). Взаимодействие асинхронных методов, в рамках языка MC#, реализуется посредством передачи сообщений с использованием каналов и обработчиков канальных сообщений. Эти каналы и обработчики определяются в MC#-программах с помощью связок в стиле языка Polyphonic C#.

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

    3.1. Модель программирования языка MC#: async- и movable-методы, каналы, обработчики связки

    В любом традиционном языке объектно-ориентированного программирования, таком, как например, C#, обычные методы являются синхронными - вызывающая программа всегда ожидает завершения вычислений вызванного метода, и только затем продолжает свою работу.

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

    Разделение всех методов в программе на обычные (синхронные) и асинхронные (в том числе, на те, которые могут быть перенесены для исполнения на другие машины) производится программистом с использованием специальных ключевых слов async и movable. (В языке MC#, семантика и использование ключевого слова async полностью совпадает с использованием этого слова в языке Polyphonic C# за тем исключением, что в MC# async-методы не могут встречаться в связках - см. об этом ниже).

    Async- и movable-методы являются единственным средством создания параллельных процессов (потоков) в языке MC#.

    Кроме средств создания параллельных процессов, любой язык параллельного программирования должен содержать конструкции

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

    Для синхронизации параллельных процессов в MC# используются связки (chords), определяемые в стиле языка Polyphonic C#.

    3.2. Async- и movable-методы

    Общий синтаксис определения async- и movable-методов в языке MC# следующий:

    модификаторы { async | movable } имя_метода ( аргументы )
    {
       < тело метода >
    }

    Ключевые слова async и movable располагаются на месте типа возвращаемого значения, поэтому синтаксическое правило его задания при объявлении метода в языке MC# имеет вид:

    return-type ::= type | void | async | movable

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

    Отличия async- и movable-методов от обычных методов состоят в следующем:

  • вызов async- и movable-методов заканчивается, по существу, мгновенно (для последних из названных методов, время затрачивается только на передачу необходимых для вызова этого метода данных на удаленную машину),
  • эти методы никогда не возвращают результаты (о взаимодействии movable-методов между собой и с другими частями программы, см. Раздел 2.2 "Каналы и обработчики").
  • Соответственно, согласно правилам корректного определения async- и movable-методов:

  • они не могут объявляться статическими,
  • в их теле не может использоваться оператор return.
  • Вызов movable-метода имеет две синтаксические формы:

  • имя_объекта.имя_метода ( аргументы )

    (место исполнения метода выбирается Runtime-системой автоматически),

  • имя_машины@имя_объекта.имя_метода ( аргументы )

    ( имя_машины задает явным образом место исполнения данного метода).

  • При разработке распределенной программы на языке MC# (т.е., при использовании в ней movable-методов и исполнении её на кластере или в Grid-сети), необходимо учитывать следующие особенности системы исполнения (Runtime-системы) MC#-программ.

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

    Поэтому, первой ключевой особенностью языка MC# (а точнее, его семантики) является то, что, в общем случае, во время вызова movable-метода, все необходимые данные, а именно:

  • сам объект, которому принадлежит данный movable-метод, и
  • его аргументы (как ссылочные, так и скалярные значения) только копируются (но не перемещаются) на удаленную машину. Следствием этого является то, что все изменения, которые осуществляет (прямо или косвенно) movable-метод с внутренними полями объекта, проводятся с полями объекта-копии на удаленной машине, и никак не влияют на значение полей исходного объекта.
  • Если копируемый (при вызове его movable-метода) объект обладает каналами или обработчиками (или же просто, они являются аргументами этого movable-метода), то они также копируются на удаленную машину. Однако, в этом случае, они становятся "прокси"-объектами для исходных каналов и обработчиков.

    3.3. Каналы и обработчики канальных сообщений

    Каналы и обработчики канальных сообщений являются средствами для организации взаимодействия параллельных распределенных процессов между собой. Синтаксически, каналы и обработчики обычно объявляются в программе с помощью специальных конструкций - связок (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#).

    3.4. Синхронизация в языке MC#

    Аналогично языку Polyphonic C#, в одной связке можно определить несколько каналов. Такого вида связки являются главным средством синхронизации параллельных (в том числе, распределенных) потоков в языке MC#:

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

    При использовании связок в языке MC# нужно руководствоваться следующими правилами их корректного определения:

  • Формальные параметры каналов и обработчиков не могут содержать модификаторов ref или out.
  • Если в связке объявлен обработчик с типом возвращаемого значения return-type, то в теле связки должны использоваться операторы return только с выражениями, имеющими тип return-type.
  • Все формальные параметры каналов и обработчика в связке должны иметь различные идентификаторы.
  • Каналы и обработчики в связке не могут быть объявлены как static.
  • 3.5. Примеры программирования на языке MC#

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

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

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

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

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

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

    Вычисление частичных сумм массива

    В этом разделе демонстрируется более сложный пример использования обработчиков для организации конвейера между процессами, представленными movable -методами.

    Рассмотрим задачу вычисления частичных сумм массива f длины n.

    А именно, по заданному массиву чисел f [ 0 : n-1 ] необходимо построить массив h [ 0 : n-1 ], такой что

    $$h[j]=\sum\limit_{i=0}^j f[i]\quad,\quad\mbox{для каждого }j:\; 0\le j<n$$

    Идея параллельного решения этой задачи состоит в разбиении массива f на p сегментов, где n кратно p, с дальнейшей одновременной обработкой этих сегментов данных длины m = n div p. Таким образом, обработка каждого сегмента будет производиться movable -методом.

    (Отметим, что приведенное ниже решение пригодно и для случая, когда n не кратно p. Соответствующее обобщение может рассматриваться в качестве упражнения).

    Разбиение исходного массива f на p сегментов производится таким образом, что в сегмент q, где ( 0 <= 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 ]

    и т.д.

    Параллельный алгоритм вычисления частичных сумм будет устроен так, что q -му процессу ( movable -методу), обрабатывающему q -ый сегмент данных, достаточно будет общаться лишь с его соседями слева и справа (соответственно, 0 -му процессу - лишь с соседом справа, а последнему, (p-1) -му процессу - лишь с соседом слева) и главной программой для возврата результатов. Процесс с номером q будет вычислять все элементы h [j] результирующего массива, такие что j mod p = q, где 0 <= j < n.

    Фрагмент главной программы, разбивающей исходный массив на сегменты и вызывающий movable -метод handleSegment, показан ниже. Здесь первым аргументом этого метода является номер сегмента, а последним - имя канала для возврата результатов.

    Объекты класса BDChannel объявляются следующим образом :

    Схема взаимодействия процессов (movable-методов) между собой и главной программой показана ниже:

    После разбиения, исходный массив f приобретает вид двумерной матрицы, распределенной по p процессам:

    процесс 0:a0,0 a0,1 a0,m-1
    процесс 1:a1,0 a1,1 a1,m-1
    ...
    процесс q:aq,0 aq,1 aq,m-1
    процесс p-1:ap-1,0 ap-1,1 ap-1,m-1

    Другими словами, эта матрица получена из массива f разрезанием его на p сегментов и транспонированием каждого сегмента.

    Ключевая идея алгоритма отдельного процесса q состоит в заполнении локальных для него массивов h0 и h1 (оба, имеющие размерность m ) в соответствии с формулами:

    $$h0[i]=\sum\limits_{j=0}^{q-1}\sum\limits_{k=0}^i a_{j,k}\quad,\quad 0\le i<m,$$ $$h1[i]=\sum\limits_{j=q+1}^{p-1}\sum\limits_{k=0}^{i-1}a_{j,k}\quad,\quad 0\le i<m.$$

    Неформально, это означает, что для процесса с номером q i -ый элемент массива h0 есть сумма всех элементов приведенной выше матрицы, которые расположены выше и слева элемента aq,i (включая и элементы столбца i ).

    Аналогично, i -ый элемент массива h1 есть сумма всех элементов матрицы, которые расположены ниже и слева элемента aq,i (но, не включая элементов из столбца i ).

    Ниже показана иллюстрация этого принципа для n = 16, p = 4 и q = 1, i = 2.

    После того, как вычислены массивы h0 и h1 (посредством взаимодействия с соседними процессами), процесс с номером q может вычислить элемент h[i * p + q] результирующего массива как

    $$h0[i]+\sum_{j=1}^i a_{q,j}+h1[i]\quad,\quad \mbox{для всех } i:\;0\le i<m.$$

    Получаемые результирующие m значений процесс q сохраняет в локальном массиве h для передачи их главной программе. Тогда общая схема movable -метода handleSegment выглядит следующим образом:

    Фрагмент программы, вычисляющий массив h0, приведен ниже.

    3.6. Сведения о практической реализации языка MC#

    Как обычно, для любого параллельного языка программирования, реализация MC# состоит из компилятора и рантайм-системы. Главными функциональными частями рантайм-системы являются:

  • ResourceManager - процесс, исполняющийся на центральном узле и распределяющий по узлам movable-методы.
  • WorkNode - процесс, исполняющийся на каждом из рабочих узлов и контролирующий выполнение movable-методов.
  • Communicator - процесс, исполняющийся на каждом из узлов и ответственный за принятие сообщений для объектов, расположенных на данном узле.
  • Компилятор переводит программу из MC# в C#, его главной целью является создание кода, реализующего:

  • выполнение movable-методов на других процессорах,
  • пересылку канальных сообщений и
  • синхронизацию методов, объединённых связкой.
  • Эти функции предоставляются соответствующими методами классов рантайм-системы. Среди них:

  • класс Session - реализует вычислительную сессию.
  • класс TCP - предоставляет возможность доставки запросов на исполнение movable-методов и канальных сообщений.
  • класс Serialization - предоставляет сериализацию/десериализацию объектов, перемещаемых на другие рабочие узлы.
  • класс Channel - содержит информацию о канале.
  • класс Handler - содержит информацию об обработчике.
  • Главные функции компилятора MC#:

  • Добавление вызовов функций Init() и Finalize() класса Session в главном методе программы. Функция Init() доставляет исполняемый модуль программы на другие узлы, запускает процесс Manager, создаёт объекты LocalNode и другие. Функция Finalize() останавливает запущенные потоки и завершает вычислительную сессию.
  • Добавление выражений, создающих объекты типа сhannel и handler для каждого из каналов и обработчиков, описанных в программе.
  • Замена вызовов async-методов на порождение соответствующих локальных потоков.
  • Замена вызовов movable-методов на запросы менеджеру распределения ресурсов.
  • Замена канальных вызовов на пересылку соответствующих сообщений по TCP-соединению. Трансляция связок, содержащих определения каналов, производится так же, как и в языке Polyphonic C#.
  • Страницы:

    Язык параллельного программирования MC# предназначен для написания программ, работающих на всём спектре параллельных архитектур - от многоядерных процессоров до Grid-сетей. Единственное требование к таким системам со стороны MC# - на них должна быть установлена среда исполнения CLR (Common Language Runtime) с соответствующим набором библиотек. На машинах с операционной системой Windows реализацией такой среды является Microsoft .NET Framework, а на машинах с операционной системой Linux - система Mono (http://www.mono-project.com), которая является свободной реализацией платформы .NET для Unix-подобных систем.

    Язык MC# является адаптацией и развитием базовых идей языка Polyphonic C# на случай параллельных и распределенных вычислений. Язык Polyphonic C# был разработан в 2002г. в Microsoft Research Laboratory (г. Кембридж, Великобритания) Н. Бентоном (N. Benton), Л. Карделли (L. Cardelli) и Ц. Фурнье (C. Fournet). Целью его создания было добавление высокоуровневых средств асинхронного параллельного программирования в язык C# для использования в серверных и клиент-серверных приложениях на базе Microsoft .NET Framework.

    Ключевая особенность языка Polyphonic C# заключается в добавлении к обычным, синхронным методам, так называемых "асинхронных" методов, которые предназначены играть в (многопоточных) программах две основные роли:

  • автономных методов, предназначенных для выполнения базовой вычислительной работы, и исполняемых в отдельных потоках, и
  • методов, предназначенных для доставки данных (сигналов) обычным, синхронным методам.
  • Для синхронизации нескольких асинхронных методов, а также асинхронных и синхронных методов, в язык C#, кроме того, были введены новые конструкции, получившие название связок (chords).

    При этом исполнение Polyphonic C#-программ, по замыслу авторов этого языка, по-прежнему, предполагалось либо на одной машине, либо на нескольких машинах, с зафиксированными на них асинхронными методами, взаимодействующими между собой с использованием средств удаленного вызова методов (RMI - Remote Method Invocation), предоставляемых библиотекой System.Runtime.Remoting платформы .NET.

    В случае языка MC#, программист может предусмотреть исполнение автономных асинхронных методов либо локально, либо удаленно. В последнем случае, метод может быть спланирован для исполнения на другой машине, выбираемой двумя способами: либо согласно явному указанию программиста (что не является типичным случаем), либо автоматически (обычно, на наименее загруженном узле кластера или машине Grid-сети). Взаимодействие асинхронных методов, в рамках языка MC#, реализуется посредством передачи сообщений с использованием каналов и обработчиков канальных сообщений. Эти каналы и обработчики определяются в MC#-программах с помощью связок в стиле языка Polyphonic C#.

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

    3.1. Модель программирования языка MC#: async- и movable-методы, каналы, обработчики связки

    В любом традиционном языке объектно-ориентированного программирования, таком, как например, C#, обычные методы являются синхронными - вызывающая программа всегда ожидает завершения вычислений вызванного метода, и только затем продолжает свою работу.

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

    Разделение всех методов в программе на обычные (синхронные) и асинхронные (в том числе, на те, которые могут быть перенесены для исполнения на другие машины) производится программистом с использованием специальных ключевых слов async и movable. (В языке MC#, семантика и использование ключевого слова async полностью совпадает с использованием этого слова в языке Polyphonic C# за тем исключением, что в MC# async-методы не могут встречаться в связках - см. об этом ниже).

    Async- и movable-методы являются единственным средством создания параллельных процессов (потоков) в языке MC#.

    Кроме средств создания параллельных процессов, любой язык параллельного программирования должен содержать конструкции

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

    Для синхронизации параллельных процессов в MC# используются связки (chords), определяемые в стиле языка Polyphonic C#.

    3.2. Async- и movable-методы

    Общий синтаксис определения async- и movable-методов в языке MC# следующий:

    модификаторы { async | movable } имя_метода ( аргументы )
    {
       < тело метода >
    }

    Ключевые слова async и movable располагаются на месте типа возвращаемого значения, поэтому синтаксическое правило его задания при объявлении метода в языке MC# имеет вид:

    return-type ::= type | void | async | movable

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

    Отличия async- и movable-методов от обычных методов состоят в следующем:

  • вызов async- и movable-методов заканчивается, по существу, мгновенно (для последних из названных методов, время затрачивается только на передачу необходимых для вызова этого метода данных на удаленную машину),
  • эти методы никогда не возвращают результаты (о взаимодействии movable-методов между собой и с другими частями программы, см. Раздел 2.2 "Каналы и обработчики").
  • Соответственно, согласно правилам корректного определения async- и movable-методов:

  • они не могут объявляться статическими,
  • в их теле не может использоваться оператор return.
  • Вызов movable-метода имеет две синтаксические формы:

  • имя_объекта.имя_метода ( аргументы )

    (место исполнения метода выбирается Runtime-системой автоматически),

  • имя_машины@имя_объекта.имя_метода ( аргументы )

    ( имя_машины задает явным образом место исполнения данного метода).

  • При разработке распределенной программы на языке MC# (т.е., при использовании в ней movable-методов и исполнении её на кластере или в Grid-сети), необходимо учитывать следующие особенности системы исполнения (Runtime-системы) MC#-программ.

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

    Поэтому, первой ключевой особенностью языка MC# (а точнее, его семантики) является то, что, в общем случае, во время вызова movable-метода, все необходимые данные, а именно:

  • сам объект, которому принадлежит данный movable-метод, и
  • его аргументы (как ссылочные, так и скалярные значения) только копируются (но не перемещаются) на удаленную машину. Следствием этого является то, что все изменения, которые осуществляет (прямо или косвенно) movable-метод с внутренними полями объекта, проводятся с полями объекта-копии на удаленной машине, и никак не влияют на значение полей исходного объекта.
  • Если копируемый (при вызове его movable-метода) объект обладает каналами или обработчиками (или же просто, они являются аргументами этого movable-метода), то они также копируются на удаленную машину. Однако, в этом случае, они становятся "прокси"-объектами для исходных каналов и обработчиков.

    3.3. Каналы и обработчики канальных сообщений

    Каналы и обработчики канальных сообщений являются средствами для организации взаимодействия параллельных распределенных процессов между собой. Синтаксически, каналы и обработчики обычно объявляются в программе с помощью специальных конструкций - связок (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#).

    3.4. Синхронизация в языке MC#

    Аналогично языку Polyphonic C#, в одной связке можно определить несколько каналов. Такого вида связки являются главным средством синхронизации параллельных (в том числе, распределенных) потоков в языке MC#:

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

    При использовании связок в языке MC# нужно руководствоваться следующими правилами их корректного определения:

  • Формальные параметры каналов и обработчиков не могут содержать модификаторов ref или out.
  • Если в связке объявлен обработчик с типом возвращаемого значения return-type, то в теле связки должны использоваться операторы return только с выражениями, имеющими тип return-type.
  • Все формальные параметры каналов и обработчика в связке должны иметь различные идентификаторы.
  • Каналы и обработчики в связке не могут быть объявлены как static.
  • 3.5. Примеры программирования на языке MC#

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

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

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

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

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

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

    Вычисление частичных сумм массива

    В этом разделе демонстрируется более сложный пример использования обработчиков для организации конвейера между процессами, представленными movable -методами.

    Рассмотрим задачу вычисления частичных сумм массива f длины n.

    А именно, по заданному массиву чисел f [ 0 : n-1 ] необходимо построить массив h [ 0 : n-1 ], такой что

    $$h[j]=\sum\limit_{i=0}^j f[i]\quad,\quad\mbox{для каждого }j:\; 0\le j<n$$

    Идея параллельного решения этой задачи состоит в разбиении массива f на p сегментов, где n кратно p, с дальнейшей одновременной обработкой этих сегментов данных длины m = n div p. Таким образом, обработка каждого сегмента будет производиться movable -методом.

    (Отметим, что приведенное ниже решение пригодно и для случая, когда n не кратно p. Соответствующее обобщение может рассматриваться в качестве упражнения).

    Разбиение исходного массива f на p сегментов производится таким образом, что в сегмент q, где ( 0 <= 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 ]

    и т.д.

    Параллельный алгоритм вычисления частичных сумм будет устроен так, что q -му процессу ( movable -методу), обрабатывающему q -ый сегмент данных, достаточно будет общаться лишь с его соседями слева и справа (соответственно, 0 -му процессу - лишь с соседом справа, а последнему, (p-1) -му процессу - лишь с соседом слева) и главной программой для возврата результатов. Процесс с номером q будет вычислять все элементы h [j] результирующего массива, такие что j mod p = q, где 0 <= j < n.

    Фрагмент главной программы, разбивающей исходный массив на сегменты и вызывающий movable -метод handleSegment, показан ниже. Здесь первым аргументом этого метода является номер сегмента, а последним - имя канала для возврата результатов.

    Объекты класса BDChannel объявляются следующим образом :

    Схема взаимодействия процессов (movable-методов) между собой и главной программой показана ниже:

    После разбиения, исходный массив f приобретает вид двумерной матрицы, распределенной по p процессам:

    процесс 0:a0,0 a0,1 a0,m-1
    процесс 1:a1,0 a1,1 a1,m-1
    ...
    процесс q:aq,0 aq,1 aq,m-1
    процесс p-1:ap-1,0 ap-1,1 ap-1,m-1

    Другими словами, эта матрица получена из массива f разрезанием его на p сегментов и транспонированием каждого сегмента.

    Ключевая идея алгоритма отдельного процесса q состоит в заполнении локальных для него массивов h0 и h1 (оба, имеющие размерность m ) в соответствии с формулами:

    $$h0[i]=\sum\limits_{j=0}^{q-1}\sum\limits_{k=0}^i a_{j,k}\quad,\quad 0\le i<m,$$ $$h1[i]=\sum\limits_{j=q+1}^{p-1}\sum\limits_{k=0}^{i-1}a_{j,k}\quad,\quad 0\le i<m.$$

    Неформально, это означает, что для процесса с номером q i -ый элемент массива h0 есть сумма всех элементов приведенной выше матрицы, которые расположены выше и слева элемента aq,i (включая и элементы столбца i ).

    Аналогично, i -ый элемент массива h1 есть сумма всех элементов матрицы, которые расположены ниже и слева элемента aq,i (но, не включая элементов из столбца i ).

    Ниже показана иллюстрация этого принципа для n = 16, p = 4 и q = 1, i = 2.

    После того, как вычислены массивы h0 и h1 (посредством взаимодействия с соседними процессами), процесс с номером q может вычислить элемент h[i * p + q] результирующего массива как

    $$h0[i]+\sum_{j=1}^i a_{q,j}+h1[i]\quad,\quad \mbox{для всех } i:\;0\le i<m.$$

    Получаемые результирующие m значений процесс q сохраняет в локальном массиве h для передачи их главной программе. Тогда общая схема movable -метода handleSegment выглядит следующим образом:

    Фрагмент программы, вычисляющий массив h0, приведен ниже.

    3.6. Сведения о практической реализации языка MC#

    Как обычно, для любого параллельного языка программирования, реализация MC# состоит из компилятора и рантайм-системы. Главными функциональными частями рантайм-системы являются:

  • ResourceManager - процесс, исполняющийся на центральном узле и распределяющий по узлам movable-методы.
  • WorkNode - процесс, исполняющийся на каждом из рабочих узлов и контролирующий выполнение movable-методов.
  • Communicator - процесс, исполняющийся на каждом из узлов и ответственный за принятие сообщений для объектов, расположенных на данном узле.
  • Компилятор переводит программу из MC# в C#, его главной целью является создание кода, реализующего:

  • выполнение movable-методов на других процессорах,
  • пересылку канальных сообщений и
  • синхронизацию методов, объединённых связкой.
  • Эти функции предоставляются соответствующими методами классов рантайм-системы. Среди них:

  • класс Session - реализует вычислительную сессию.
  • класс TCP - предоставляет возможность доставки запросов на исполнение movable-методов и канальных сообщений.
  • класс Serialization - предоставляет сериализацию/десериализацию объектов, перемещаемых на другие рабочие узлы.
  • класс Channel - содержит информацию о канале.
  • класс Handler - содержит информацию об обработчике.
  • Главные функции компилятора MC#:

  • Добавление вызовов функций Init() и Finalize() класса Session в главном методе программы. Функция Init() доставляет исполняемый модуль программы на другие узлы, запускает процесс Manager, создаёт объекты LocalNode и другие. Функция Finalize() останавливает запущенные потоки и завершает вычислительную сессию.
  • Добавление выражений, создающих объекты типа сhannel и handler для каждого из каналов и обработчиков, описанных в программе.
  • Замена вызовов async-методов на порождение соответствующих локальных потоков.
  • Замена вызовов movable-методов на запросы менеджеру распределения ресурсов.
  • Замена канальных вызовов на пересылку соответствующих сообщений по TCP-соединению. Трансляция связок, содержащих определения каналов, производится так же, как и в языке Polyphonic C#.
  • Вернуться к учебному плану