Новые средства языка MC#: async- и movable-методы, каналы и обработчики
Новые средства языка MC#: async- и movable-методы, каналы и обработчики
В любом традиционном языке объектно-ориентированного программирования, таком, как например, C#, обычные методы являются синхронными - вызывающая программа всегда ожидает завершения вычислений вызванного метода, и только затем продолжает свою работу.
При исполнении программы на параллельной архитектуре, сокращение времени её работы может быть достигнуто путем распределения множества исполняемых методов на несколько ядер одного процессора, и, возможно, отправкой части из них на другие процессоры (машины) при распределенных вычислениях (Рис 4.1):
Разделение всех методов в программе на обычные (синхронные) и асинхронные (в том числе, на те, которые могут быть перенесены для исполнения на другие машины) производится программистом с использованием специальных ключевых слов async и movable. (В языке MC#, семантика и использование ключевого слова async полностью совпадает с использованием этого слова в языке Polyphonic C# за тем исключением, что в MC# async-методы не могут встречаться в связках - см. об этом ниже).
Async- и movable-методы являются единственным средством создания параллельных процессов (потоков) в языке MC#.
Кроме средств создания параллельных процессов, любой язык параллельного программирования должен содержать конструкции
- для обеспечения взаимодействия параллельных процессов между собой,
- для их синхронизации.
Основой взаимодействия параллельных процессов в языке MC# является передача сообщений (в отличие от другой альтернативы - использования общей (разделяемой) памяти). Продолжая сравнение языка MC# с языком X10, следует отметить, что в языке X10 реализованы обе парадигмы взаимодействия - как на основе передачи сообщений, так и на основе общей (разделяемой) памяти. В языке MC#, средства взаимодействия между процессами оформлены в виде специальных синтаксических категорий - каналов и обработчиков канальных сообщений. При этом, синтаксически посылка сообщения по каналу или прием из него с помощью обработчика выглядят в языке как вызовы обычных методов. В языке X10, пересылка значений с одного "места" (place - в терминологии X10) в другое, требует явного порождения (асинхронной) активности, осуществляющей транспортировку этого сообщения.
Для синхронизации параллельных процессов в MC# используются связки (chords), определяемые в стиле языка Polyphonic C#. В языке X10, для синхронизации используются более сложные конструкции под названием clocks.
4.1. Async- и movable-методы
Общий синтаксис определения async- и movable-методов в языке MC# следующий:
модификаторы { async | movable } имя_метода ( аргументы ) { < тело метода > }
Ключевые слова async и movable располагаются на месте типа возвращаемого значения, поэтому синтаксическое правило его задания при объявлении метода в языке MC# имеет вид:
return-type ::= type | void | async | movable
Задание ключевого слова async означает, что при вызове данного метода он будет запущен в виде отдельного потока локально, т.е., на данной машине (возможно, на отдельном ядре процессора), но без перемещения на другую машину. Ключевое слово movable означает, что данный метод при его вызове может быть спланирован для исполнения на другой машине.
Отличия async- и movable-методов от обычных методов состоят в следующем:
- вызов async- и movable-методов заканчивается, по существу, мгновенно (для последних из названных методов, время затрачивается только на передачу необходимых для вызова этого метода данных на удаленную машину),
- эти методы никогда не возвращают результаты (о взаимодействии movable-методов между собой и с другими частями программы, см. Раздел 4.2 "Каналы и обработчики").
Соответственно, согласно правилам корректного определения async- и movable-методов:
- они не могут объявляться статическими,
- в их теле не может использоваться оператор return.
Вызов movable-метода имеет две синтаксические формы:
-
имя_объекта.имя_метода ( аргументы )
(место исполнения метода выбирается Runtime-системой автоматически),
-
имя_машины@имя_объекта.имя_метода ( аргументы )
( имя_машины задает явным образом место исполнения данного метода).
При разработке распределенной программы на языке MC# (т.е., при использовании в ней movable-методов и исполнении её на кластере или в Grid-сети), необходимо учитывать следующие особенности системы исполнения (Runtime-системы) MC#-программ.
Во-первых, объекты, создаваемые во время исполнения MC#-программы, являются, по своей природе статическими: после своего создания, они не перемещаются и остаются привязанными к тому месту (машине), где они были созданы. В частности, именно в этом месте (на этой машине) они регистрируются Runtime-системой, что необходимо для доставки канальных сообщений этим объектам и чтения сообщений с помощью обработчиков, связанных с ними (этими объектами).
Поэтому, первой ключевой особенностью языка MC# (а точнее, его семантики) является то, что, в общем случае, во время вызова movable-метода, все необходимые данные, а именно:
- сам объект, которому принадлежит данный movable-метод, и
- его аргументы (как ссылочные, так и скалярные значения) только копируются (но не перемещаются) на удаленную машину. Следствием этого является то, что все изменения, которые осуществляет (прямо или косвенно) movable-метод с внутренними полями объекта, проводятся с полями объекта-копии на удаленной машине, и никак не влияют на значение полей исходного объекта.
Эта ситуация проиллюстрирована на Рис 4.2.
Поэтому выполнение программы
даст
Before: x = 1 After: x = 1
Если копируемый (при вызове его movable-метода) объект обладает каналами или обработчиками (или же просто, они являются аргументами этого movable-метода), то они также копируются на удаленную машину. Однако, в этом случае, они становятся "прокси"-объектами для исходных каналов и обработчиков (см. об этом подробнее в Разделе 4.2).
В распределенном режиме, имеется два режима параллелизации MC#-программ: "функциональный" и "нефункциональный" (или объектный), и от выбора того или другого будет, в конечном счете, зависеть эффективность исполнения программы. Эти режимы задаются модификаторами functional и nonfunctional при объявлении movable-метода (для async-методов эти модификаторы игнорируются). Значением по умолчанию является режим functional.
В функциональном режиме, объект, для которого вызывается movable-метод, не передается на удаленную машину. То есть, все данные, необходимые movable-методу, должны передаваться через его аргументы. Наоборот, путем задания модификатора nonfunctional, обеспечивается передача объекта на удаленную машину.
При использовании MC#-программы на кластерных архитектурах, которые обычно состоят из главной машины (фронтенда) и подчиненных ей узлов, имеется специфика в вызове movable-метода с явным заданием места его исполнения - в этом случае, должны указываться как имя главной машины, так и имя узла в формате
имя_машины:имя_узла@имя_объекта.имя_метода ( аргументы )
4.2. Каналы и обработчики
Каналы и обработчики канальных сообщений являются средствами для организации взаимодействия параллельных распределенных процессов между собой. Синтаксически, каналы и обработчики обычно объявляются в программе с помощью специальных конструкций - связок ( chords ). Впервые, в императивных языках, конструкция связок появилась в языке Polyphonic C#. Помимо объявления каналов и обработчиков, связки также играют в языке роль средств синхронизации процессов: как распределенных (использующих movable-методы), так и нераспределенных (использующих async-методы).
Например, объявление канала sendInt для передачи одиночных целочисленных значений вместе с соответствующим обработчиком getInt для получения значений из этого канала, выглядит следующим образом:
В общем случае, синтаксические правила определения связок в языке 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, а потому они всегда привязаны к некоторому объекту класса, в рамках которого они объявлены:
Таким образом, мы можем послать целое число n по каналу sendInt, записав выражение
a.sendInt( n );
где a есть объект для которого определен канал sendInt.
Если имя канала использовано без уточняющих префиксов, например, как
c( n );
то подразумевается, как обычно, использование текущего объекта:
this.c ( n );
Обработчик используется для приема значений (возможно, предобработанного с помощью кода, являющегося телом связки) из канала (или группы каналов), совместно определенных с этим обработчиком. Например, для приема значения из канала sendInt можно записать
int m = a.getInt();
Если, к моменту вызова обработчика, связанный с ним канал пуст (т.е., по этому каналу значений не поступало или они все были выбраны посредством предыдущих обращений к обработчику), то этот вызов блокируется. Когда по каналу приходит очередное значение, то происходит исполнение тела связки (которое может состоять из произвольных вычислений) и по оператору return происходит возврат результирующего значения обработчику.
Наоборот, если к моменту прихода значения по каналу, нет вызовов обработчика, то это значение просто сохраняется во внутренней очереди канала, где, в общем случае, накапливаются все сообщения, посылаемые по данному каналу. При вызове обработчика и при наличии значений во всех каналах соответствующей связки, для обработки в теле этой связки будут выбраны первые по порядку значения из очередей каналов.
Следует отметить, что, принципиально, срабатывание связки, состоящей из обработчика и одного или нескольких каналов, возможно в силу того, что они вызываются, в типичном случае, из различных потоков.
Аналогично языку Polyphonic C#, в одной связке можно определить несколько каналов. Такого вида связки являются главным средством синхронизации параллельных (в том числе, распределенных) потоков в языке MC#:
Таким образом, общее правило срабатывания связки состоит в следующем: тело связки исполняется только после того, как вызваны все методы из заголовка этой связки.
Приведенный выше пример иллюстрирует случай, когда один обработчик объявлен для нескольких каналов:
Также, возможно объявление канала, разделяемого между несколькими обработчиками:
Синтаксически, такое объявление возможно путем применения двух связок, как показано в примере ниже:
Таким образом, в данном примере, если имеются значения в каналах c1 и c2, то возможно срабатывание обработчика h1. Аналогичная ситуация имеет место для каналов c2, c3 и обработчика h2. В общем случае, это может привести к нетерминизму в поведении программы.
Наконец, возможен случай, когда один и тот же обработчик объявлен в разных связках и с различными каналами:
Схематически, совокупность таких определений может быть представлена как на Рис 4.6:
В этом случае, вызов обработчика при наличии значения хотя бы в одном из каналов, приведет к срабатыванию соответствующей связки. Легко заметить, что и здесь наличие значения более, чем в одном канале, может стать источником недетерминизма в поведении программы.
При использовании связок в языке MC# нужно руководствоваться следующими правилами их корректного определения:
- Формальные параметры каналов и обработчиков не могут содержать модификаторов ref или out.
- Если в связке объявлен обработчик с типом возвращаемого значения return-type, то в теле связки должны использоваться операторы return только с выражениями, имеющими тип return-type.
- Все формальные параметры каналов и обработчика в связке должны иметь различные идентификаторы.
- Каналы и обработчики в связке не могут быть объявлены как static.
Вторая ключевая особенность языка MC# состоит в том, что каналы и обработчики могут передаваться в качестве аргументов методам (в том числе, async- и movable -методам) отдельно от объектов, которым они принадлежат (в этом смысле, они похожи на указатели на функции в языке С, или, в терминах языка C#, на делегатов ( delegates ) ).
Третья ключевая особенность языка MC# состоит в том, что, в распределенном режиме, при копировании каналов и обработчиков на удаленную машину (под которой понимается узел кластера или некоторая машина в Grid-сети) автономно или в составе некоторого объекта, они становятся прокси-объектами, или посредниками для оригинальных каналов и обработчиков. Такая подмена скрыта от программиста - он может использовать переданные каналы и обработчики (а, в действительности, их прокси-объекты) на удаленной машине (т.е., внутри movable-методов) также, как и оригинальные: как обычно, все действия с прокси-объектами перенаправляются Runtime-системой на исходные каналы и обработчики. В этом отношении, каналы и обработчики отличаются от обычных объектов: манипуляции над последними на удаленной машине не переносятся на исходные объекты (см. первую ключевую особенность языка MC#).
На Рис 4.7 и 4.8 схематически демонстрируются передача и использование каналов и обработчиков на удаленной машине. Верхние индексы y у имен каналов и обработчиков обозначают исходную машину, где они были созданы.
Рис. 4.7. Посылка сообщения по удаленному каналу: (0) копирование канала на удаленную машину, (1) посылка сообщения по (удаленному) каналу, (2) перенаправление сообщения на исходную машину.
Рис. 4.8. Чтение сообщения из удаленного обработчика: (0) копирование обработчика на удаленную машину, (1) чтение сообщения из (удаленного) обработчика, (2) перенаправление операции чтения на исходную машину, (3) получение прочитанного сообщения с исходной машины, (4) возврат полученного сообщения.
Этих средств и механизмов оказывается достаточно для организации взаимодействия произвольной сложности между параллельными, распределенными процессами, что демонстрируется в примерах следующего раздела.