![]() |
|
Механизмы взаимодействия объектов в параллельном объектно-ориентированном языке программирования MC#
Механизмы
взаимодействия объектов в параллельном объектно-ориентированном языке
программирования MC# Общий обзор языка
программирования MC# Классификация каналов в
языке MC# Описание типов каналов с
примерами их применения Дополнительные встроенные
классы Создание пользовательских
каналов Обзор других языков,
использующих конструкцию “канал” для взаимодействия процессов/объектов ВведениеБазовым механизмом взаимодействия
параллельных процессов в Общий обзор языка программирования MC#Язык MC# базируется на модели программирования, предложенной Н. Бентоном (N.Benton), Л. Карделли ( L. Cardelli ) и Ц. Фурнье ( C. Fournet ) в языке Polyphonic C# [POLY]. Целью создания этого языка было добавление высокоуровневых средств асинхронного параллельного программирования в язык C#, входящий в состав программной платформы .NET фирмы Microsoft. Ключевая особенность языка Polyphonic C# заключается в добавлении к обычным синхронным методам, так называемых "асинхронных" методов, которые предназначены играть в программе две основные роли: 1) автономных методов, исполняемых в отдельных потоках; 2) методов, предназначенных для доставки данных обычным, синхронным методам. Для синхронизации методов обоих видов в язык C#, кроме того, были добавлены новые конструкции, получившие название связок (chords). Связки – это набор заголовков функций, объединённых символами ‘&&’, причём у связки существует только одно тело функции. Тело функции исполняется только тогда, когда будут вызваны все методы, входящие в заголовок связки. В связке может участвовать не более одного синхронного метода, который может возвращать какое-либо значение – результат исполнения тела связки. Более подробно ознакомиться с этими конструкциями можно на странице [POLY]. Основным нововведением в MC# стало введение дополнительных “перемещаемых” методов (movable-методов), которые могут исполняться на любом из узлов кластера (или GRID-сети), а также каналов для возврата результатов из movable-функций и способа объединения каналов в связки с обычными синхронными методами. Как оказалось, этого вполне достаточно, чтобы создавать простые, но в тоже время полнофункциональные программы, которые к тому же могут использовать всю мощь кластерных технологий. Кроме того, Runtime-система MC# обеспечивает динамическое распараллеливание, т.е. только на этапе исполнения в зависимости от загруженности узлов решается вопрос, на каком из них должна исполняться конкретная movable-функция. Ранее такой подход уже использовался в других проектах (например, в T-системе [TSYS], [ABRA]) и доказал свою эффективность. Классификация каналов в языке MC#В MC# для передачи данных между объектами, располагающимися на разных узлах, используются каналы. Каналы обязательно привязаны к узлу, на котором они были созданы, т.е. все сообщения, отсылаемые по каналу, отправляются сначала на узел привязки. Копии каналов могут быть переданы на другой узел в качестве параметров movable-методам и впоследствии, использоваться на удалённом узле для получения и отправления сообщений. Таким образом, в MC# каналы являются средством общения между объектами на разных узлах. При “переходе” на другой узел канал регистрируется локально и может быть использован повторно. Все каналы подразделяются на однонаправленные и двунаправленные каналы. Пользователь может только посылать свои данные в однонаправленные каналы, но не может с них “напрямую” читать – чтение производится при срабатывании связок, в которых участвует данный канал. Над двунаправленными каналами можно производить обе операции - как чтение, так и запись. Один и тот же канал может участвовать в нескольких связках, что позволяет создавать каналы с приоритетами - задействована будет та связка, которая объявлена раньше в исходном коде (однако, такой способ имеет свои недостатки, о которых будет рассказано в главе “Каналы с приоритетами”). Далее, по типу очередей каналы можно подразделить на следующие подтипы: - каналы с ограниченной ёмкостью накопителя; - с неограниченной ёмкостью накопителя; - каналы без накопителей. Каналы без накопителей, как таковых, обычно содержат только последний прибывший объект, который не удаляется при чтении, но заменяется другим объектом при поступлении нового сообщения. Каналы могут быть асинхронными и синхронными. В первом случае, сразу же после вызова Send (метода отправки какого-либо объекта), поток не дожидаясь, пока сообщение будет прочитано, продолжает своё исполнение. Во втором, поток приостанавливает свою работу до тех пор, пока сообщение не будет прочитано каким-либо другим процессом. По умолчанию, в MC# по каналам могут быть переданы объекты любых типов (поддерживающих сериализацию). Но канал может быть типизированным, т.е. по нему можно послать данные только определённых типов. Более того, они могут фильтровать данные или производить какие-либо действия с проходящими через них данными. В MC# пользователь может сам создавать свои собственные каналы, наделяя их той или иной функциональностью, размером очередей и т.д. (более подробно об этом см. в разделе “Создание пользовательских каналов”). Описание типов каналов с примерами их примененияChannelКласс Channel является основным в иерархии
однонаправленных каналов языка MC#.
Все остальные однонаправленные каналы должны быть унаследованы от этого класса.
Отличие однонаправленных каналов от двунаправленных в том, что пользователь не
может “вручную” произвести из него чтение. Фактически чтение из каналов,
унаследованных от класса Channel, может производиться только с помощью связок (т.е. чтение
производится, когда будет задействована та или иная связка). Как и асинхронные методы в Polyphonic C#, каналы могут участвовать в связках. Связка состоит из заголовка и тела метода, где заголовок – несколько объявлений методов, разделённых символом ‘&’:
Тело связки выполняется только когда все методы, входящие в связку были вызваны. Одиночные вызовы методов выстраиваются в очередь до тех пор, пока не будут удовлетворять заголовку какой-либо связки. В любой связке только один метод может быть синхронным. В потоке, связанным с этим методом исполняется тело связки. Возвращаемое в теле связки значение становится возвращаемым значением синхронного метода. В случае если в связке отсутствует синхронный метод, а по всем каналам, входящим в данную связку поступили новые сообщения, то автоматически будет исполнено тело связки в новом потоке. При этом тело связки не возвращает никуда результат своей работы, однако эта особенность может оказаться очень удобной, например, для прорисовки пользовательского интерфейса. В следующем примере при вызове функции mfun должны распечататься числа от 0 до 99:
В языке Polyphonic C# не существовало понятия
“канал”, а ключевое слово
Отличие каналов от async лишь в том, что канал может быть переправлен на другие узлы, и сообщения в этот канал могут отправляться с любого узла, где имеется копия этого канала. Каждый канал имеет
два уникальных идентификатора: primaryGuid и guid. Первый из них (primaryGuid) – это идентификатор данного канала, на
узле, где он впервые был создан. При отправлении какого-либо сообщения по
каналу, оно становится в канальную очередь на узле первоначального создания
(поиск канала на этом узле производится по primaryGuid). Идентификатор guid генерируется автоматически при каждом
“мигрировании” канала на новый узел. Этот идентификатор используется при чтении
из удалённого источника. При отправлении
какого-либо сообщения по каналу, оно обязательно отправляется на тот узел, где
первоначально был создан канал. Этот факт следует учитывать при написании
программ на языке MC#. По каналу можно
послать сообщения при помощи метода Send. Метод Send
возвращает true, если объект был
доставлен, в противном случае он возвращает false. Метод Put используется для постановки какого-либо
объекта в локальную очередь – его можно переопределить при написании
пользовательских типов каналов. Из приведённого
ниже фрагмента интерфейса класса Channel видно, что, вообще говоря, по каналу может быть отправлен объект
любого типа (типизация каналов будет добавлена с выходом платформы .Net, поддерживающей темплейты в языке C#). Для проверки на соответствие типов (а
также для проверки значений отсылаемых значений) можно использовать метод CheckValues, который вызывается каждый раз перед
отправлением какого-либо сообщения. Если CheckValues возвращает значение true, то данные отсылаются на другой узел и
становятся на том узле в локальную очередь.
BDChannelКласс BDChannel является основным для всех двунаправленных каналов в иерархии каналов MC#. В классе BDChannel дублируются все свойства и методы класса Channel. Единственным добавлением в интерфейсе этого класса является метод Receive, производящий чтение из потока. В случае если канальная очередь пуста, вызов этого метода блокирует поток до тех пор, пока не поступит новый объект и не произведётся чтение из канала. Экземпляры класса BDChannel можно также передавать в качестве параметров movable-функциям сколько угодно раз. Следует заметить, что
двунаправленные каналы, в отличие от однонаправленных каналов, не могут
участвовать в связках. Ниже приведён пример создания и передачи канала в movable-метод.
Данный пример должен напечатать “Hello world!” BoundedChannelВ определённом типе задач часто
необходимо ограничить ёмкость накопителя какого-либо канала. Для этих целей
существует класс
Этот код должен вывести последние десять чисел, поступившие в канал (т.е. числа от 89 до 99). Аналогично, двунаправленный канал с длиной очереди равной 10, создаётся следующим образом:
Использовать TransientChannelИногда не нужно хранить все
заявки в очереди, а достаточно только хранения последней поступившей заявки.
Для этой цели служат каналы типа Пусть, например, у нас имеется movable-функция
Приведённый выше код при вызове
функции Аналогично, можно создать двунаправленный канал, у которого не будет очереди сообщений:
SynchronizedChannelБазовые каналы языка МС# являются
асинхронными. При посылке сообщения по каналам типа Для этих целей в MC# служат каналы типа На самом деле, синхронизированные каналы - это исходный тип каналов, который был принят в p-исчислении, когда оно появилось (и даже в более ранних исчислениях). Однако затем появились варианты исчислений с асинхронными каналами, что объяснялось двумя причинами: а) во-первых, асинхронные каналы являются более базовыми единицами, чем синхронные каналы, другими словами, синхронные каналы выразимы через асинхронные каналы, но не наоборот (пример этого см. ниже); б) во-вторых, асинхронная передача сообщений лежит в основе механизма взаимодействия процессов (машин), принятого в реальных компьютерах.
В следующем примере имеется две
Дополнительные встроенные классыДля удобства программирования в язык был добавлен набор классов, обеспечивающий наиболее часто используемые функции, оперирующие с каналами, такие как перенаправление потоков данных, дублирование потоков и т.д. Все классы встроены в Runtime-систему и в основном реализованы на уровне протокола передачи данных в виде отдельных объектов. ActivityРанее, в разделе, описывающем тип
Ниже приведён пример использования класса
Данный пример должен выдать следующее:
RedirectorПри программировании определённых
задач иногда необходимо перенаправить весь поток данных из одного канала
(канала-поставщика) в другой (канал-потребитель). Для этой цели можно
использовать объект класса
Приведённый фрагмент должен
напечатать строку “ Multiplier
Класс
Приведённый код должен выдать ответ, в котором печатаются числа от 0 до 9 в функциях fun2 и fun3 (распечатанные числа не обязательно должны быть упорядоченными, более того порядок может меняться при каждом запуске, т.к. все действия производятся параллельно):
Создание пользовательских каналовВ языке MC# можно создавать свои собственные
каналы, наследуясь от уже созданных каналов и
добавляя или изменяя логику проверки, хранения и пересылки данных.
Пользовательские однонаправленные каналы также могут участвовать в связках, как
и встроенные каналы. Пользовательские каналы также можно передавать с одного
узла на другой – все необходимые действия, связанные с локальной регистрацией
каналов уже реализованы в классе Фильтрация данныхИногда необходимо предотвратить поступление каких-либо данных по каналу. Это можно сделать вручную, т.е. постфактум, когда данные уже поступили на узел, где впервые был зарегистрирован канал и производится чтение из этого канала. Но в таком случае происходит ненужное срабатывание связок. Чтобы предотвратить это в языке MC# имеется возможность создавать свои собственные каналы, в которых проверка данных происходит ещё при отправке данных. Если данные не удовлетворяют определённым критериям, то данные не отсылаются, и, следовательно, не происходит срабатывания связок. Следующий пример демонстрирует создание, и использование однонаправленного канала, фильтрующего все отрицательные числа:
В данном фрагменте связка с Get не будет задействована при вызове метода cc.Send и метод Send должен возвратить значение false. Аналогично можно создавать двунаправленные каналы, которые будут производить аналогичную фильтрацию:
Пример аккумулирующего потокаПотоки по мере своей работы могут
накапливать определённую статистическую информацию. Например, это может быть
подсчёт количества проходящих сообщений, размера переданных данных. Для этих
целей в классе -
-
-
-
Приведём фрагмент кода,
демонстрирующий использование метода
При вызове функции
Каналы с приоритетамиВ данном разделе демонстрируется пример эмулирования каналов с приоритетами с помощью связок на примере задачи Санта Клауса. Формулировка задачиПервоначально сформулированная Trono [TRON], задача Санта Клауса является интересным (и забавным) упражнением в параллельном программировании, которое привлекает к себе внимание чаще, чем традиционные задачи на взаимное исключение, т.к. оно включает в себя три вида процессов, а также некоторое количество процессов для взаимодействия. Задача может быть сформулирована следующим образом:
Санта Клаус постоянно спит, до
тех пор, пока не будет разбужен либо всеми девятью северными оленями,
вернувшимися из своего отпуска, либо любыми тремя эльфами из его десяти эльфов.
Если Санту разбудили олени, то он запрягает их всех в свои сани, доставляет
игрушки и, наконец, распрягает оленей (отправляя их обратно в отпуск). Если же
его разбудила группа эльфов, он провожает каждого из них в свой кабинет,
консультируется с ними по поводу игрушек. После этого он выводит каждого эльфа
из своего кабинета (отправляя их обратно работать). Санта должен отдавать приоритет
оленям, в случае если его ожидают сразу и олени, и группа эльфов. Ошибки, с которыми легко можно столкнуться, пытаясь решить эту задачу, включают случаи, когда не учитывается, что лишние эльфы могут проникнуть в группу эльфов как раз в тот момент, когда Санта начал провожать её в свой кабинет. Или, например, случай, когда Санта может отправиться развозить игрушки, в то время, когда некоторые из оленей всё ещё дожидаются его на стоянке. Ben-Ari [BENA] указывая на ошибку (второго вида) в первоначальном решении Trono, базирующемся на семафорах, показал, как задача может быть аккуратно решена с использованием параллельных примитивов языка Ada, и сравнил его с менее эллегантным и менее эффективным решением на языке Java. Вспомогательный классМы начнём с описания класса nway, который позволит одному потоку синхронизироваться с другими потоками. Поток может вызвать метод acceptn( m ) какого-либо экземпляра класса nway, после чего будет заблокирован до тех пор, пока m других потоков не вызовут метод entry(). В свою очередь, вызовы метода entry() будут блокироваться до тех пор, пока не будет вызван метод acceptn( m ).
Определение класса nway демонстрирует обычный подход в Polyphonic C# и MC# - использование приватных сообщений для фиксации состояния объекта. Вызов метода acceptn( m ) генерирует новое приватное сообщение tokens( m ). Каждый вызов метода entry() ожидает появление сообщения tokens( k ), после появления которого генерируется новое сообщение вида tokens( k - 1 ) или allgone(). Сообщение allgone() при срабатывания связки wait/allgone снимает блок с вызова метода wait(). Основное решениеМы будем использовать экземпляры класса nway для синхронизации Санты со всеми оленями на стадии, когда их запрягают и распрягают, и со всеми эльфами в группе в то время, когда их провожают в офис и из офиса:
Теперь, каждый из потоков-эльфов исполняет следующий код:
Вызов метода elfqueue() используется для контроля над объединением эльфов в группы по трое и пробуждением Санты Клауса. Он синхронизируется с другим приватным сообщением, содержащим информацию о количестве ожидающих эльфов:
Первоначально поступает сообщение elveswaiting( 0 ). Каждый из эльфов, вызывая метод elfqueue(), будет ожидать поступления сообщения elveswaiting( k ) после чего либо отправит сообщение elveswaiting( k + 1 ), либо elvesready(), что послужит сигналом для пробуждения Санты. Заметим, что в последнем случае сообщение elveswaiting() не посылается - другие эльфы, вызвавшие метод elfqueue() будут заблокированы, до тех пор, пока текущую группу не проводят в офис. Код для потоков-оленей будет приблизительно тем же, что и для эльфов. Одно сообщение reinwaiting( k ) подсчитывает сколько оленей уже вернулось из отпуска, в то время как сообщение reindeerready() используется для уведомления Санты, что все они вернулись:
Санта просто исполняет следующее:
где синхронный метод waittobewoken() определяется двумя связками, одна из которых синхронизирована с reindeerready(), а другая с elvesready():
Каждое из тел связок содержит соответствующие действия, включая сброс счётчика ожидающих эльфов или оленей. Место размещения вызова reinwaiting(0) не является критическим - можно поместить его где угодно вне связок или даже рядом с вызовом reindeerready() в связке reindeerback(). С другой стороны, чтобы предотвратить ошибку, связанную с изменением размера очереди, нельзя вызывать elveswaiting( 0 ) до того момента, пока всех трёх эльфов из группы не проводят в офис. Размещение этого вызова так, как показано выше, позволяет набирать новую группу, в то время как Санта консультирует предыдущую группу. ПриоритетыВ текущей реализации языков Polyphonic C# и MC#, в решении, приведённом в предыдущем пункте, действительно отдаётся предпочтение потокам-оленям, как того требует формулировка задачи. Происходит это потому, что при проверке срабатывания связок связки проверяются последовательно в том порядке, в котором они описаны в исходном коде. А в нашем случае связка соответствующая waittobewoken() вместе с reindeerready() предшествует связке с методом elvesready(). Таким образом, если сообщение elvesready() стояло бы первым, которое отвечает за возобновление работы потока Санты, и если бы появилось сообщение reindeerready() когда поток-Санта только начинает своё исполнение, то будет исполнена связка с оленями. Такое поведение программы наблюдается на практике, если заменить порядок следования связок. Полагаться на алгоритмы проверки срабатывания связок не приемлемо: они не являются частью официальной семантики языков и версия компилятора, пытающегося применить оптимизирующие алгоритмы, наверняка не будет следовать порядку описания связок в исходном тексте. Более того, такой подход неуклюж и является незащищённым от ошибок способом контроля над приоритетами, а в более сложных случаях может и не быть текстовой зависимости, с помощью которой можно получить требуемые приоритеты. К счастью, программная реализация приоритетов довольно легка. Для этого мы введём одно новое сообщение reindeernotready() и добавим его для синхронизации между Сантой и собравшейся группой эльфов:
Мы принимаем сообщение reindeernotready() когда олени прибыли:
и добавим отправку сообщения reindeernotready() в инициализирующий код и в код потоков Санты/оленей, который теперь будет выглядеть следующим образом:
Эти дополнительные семь строчек - это всё, что нам надо для того, чтобы контролировать приоритеты вручную. Приведённый выше пример решения задачи Санта-Клауса является локальным. В более сложном случае, если,
допустим, мы захотим, чтобы потоки эльфов или оленей запускались на других
узлах, то придётся добавить ещё несколько связок с использованием Обзор других языков, использующих конструкцию “канал” для взаимодействия процессов/объектовТеоретической основой всех языков
программирования с обменом сообщениями по каналам является p-исчисление
[PICA]. Фактически,
p-исчисление
– это математическая модель процессов, взаимодействующих между собой и его
использование не ограничивается только программами. Простейшей операцией в p-исчислении
является передача канала от одного процесса к другому, принимающая сторона после
получения канала может использовать в дальнейшем этот канал для взаимодействия
с другими процессами. Это свойство, а также то, что исчисление является число
математическим языком для описания процессов, позволяет описывать любые
процессы, в которых некоторые затрагиваемые ресурсы изменяются с течением
времени. На базе p-исчисления было создано много других исчислений и языков программирования: PICT, Facile, Join, Ambients, Spi, POOL и т.д. Интересным проектом является TYped Concurrent Objects [TYCO] – расширение p-исчисления с добавлением распределённых структур и рекурсии (вместо репликации p-исчисления). В основном, все эти языки заимствуют семантику или центральные аспекты p-исчисления и нацелены на определённую область приложений. Существует довольно много диалектов языка ML, которые поддерживают каналы. Из них основными являются JoCaml [JOCA] и Nomadic Pict. Язык JoCaml является функциональным аналогом языка Polyphonic C#. Каналы в нём имеют свои “уникальные” имена. Любой процесс, который узнал имя канала, может послать по нему сообщение. Для каждого канала создаётся специальный процесс, который следит за поступающими сообщениями. Как только приходит сообщение, запускается копия процесса и производится обслуживание сообщения. Аналогично языку MC# в JoCaml имеются средства для синхронизации потоков данных и их перенаправления из одного канала в другой. С одной стороны в языке JoCaml имеется возможность вызвать какой-либо метод на определённом узле, что отсутствует в языке MC# (Runtime система сама решает, на каком из узлов должен исполняться данный movable-метод). В JoCaml также имеются встроенные средства для обнаружения сбоев в программе на определённом узле, что так же на данный момент отсутствует в MC# (аналогичные средства появятся в следующих версиях Runtime-системы). С другой стороны недостатком языка JoCaml является отсутствие автоматической балансировки нагрузки между узлами – её должен реализовывать сам программист в каждой отдельной задаче, что само по себе является нетривиальной задачей. К тому же, язык MC# базируется на языке C#, т.е. он может использовать многочисленные библиотеки, написанные на любом из языков, поддерживаемых платформой .Net. Список литературы[MCSH] – Официальный сайт языка программирования MC# http://u.pereslavl.ru/~vadim/MCSharp [MCS2] – MC#: расширение языка C# для программирования
на кластерных и GRID-архитектурах, Технологии C# и .NET '2003, 1-ая
Международная конференция технологий C# и .NET по Алгоритмам,
Компьютерной Графике, Визуализации, Распределённым и WEB
вычислениям, Гузев В. Б., Сердюк Ю. П., Чудинов А. М., Plzen, Czech Republic. ISBN 80-903100-3-6 Web-версия: http://u.pereslavl.ru/~vadim/MCSharp/docs/index.ru.html
[TSYS] – Официальный сайт проекта SKIF [POLY] – Официальный сайт языка программирования Polyphonic C# http://research.microsoft.com/~nick/polyphony/ [CALC] – Calculi for Mobile Processes – сайт, посвящённый p-исчислению. [PICA] – Joachim Parrow, An introduction
to p-Calculus, - chapter to appear in Handbook
of Process Algebra, ed. Bergstra, Ponse and Smolka, Elsevier [MODE] – N. Benton, L. Cardelli, C.
Fournet "Modern concurrency abstractions for C#", - draft submitted
to ACM Transactions on Programming Languages and Systems, July 2002. [ABRA] – С.М.Абрамов, А.И. Адамович "Т-система - среда программирования с поддержкой автоматического динамического распараллеливания программ", - В сб. "Программные системы: Теоретические основы и приложения", под ред. А.К. Айламазяна. М.: Наука. Физматлит, 1999, стр. 201 - 213. [JOCA] – Cedric Fournet, Fabrice Fessant, Luc Maranget, Alan Smith, “JoCaml: a Language for Concurrent Distributed And Mobile Programming” [TYCO] – Сайт проекта TYped Concurrent Objects (TYCO) http://www.di.fc.ul.pt/~vv/tyco.html [TRON] J. A. Trono. A new exercise in
concurrency. SIGCSE Bulletin, 26(3):8-10,
1994. Corrigendum: 26(4):63. [SANT] Nick Benton. Jingle Bells:
Solving the Santa Claus Problem in Polyphonic C#, March 20, 2003 [BENA] M. Ben-Ari. How to solve the
Santa Claus problem. Concurrency: Practice & Experience, 10(6):485-496,
1998. [ECMA] ECMA. Standard ECMA-334: C#
Language Specification, December 2001. Приложение
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|