Разработанный в Федеральной политехнической школе Лозанны (Швейцария) язык sС++ (http://ltiwww.epfl. ch/sCxx) содержит ядро реального времени, многое заимствует от современных теорий моделирования и анализа [1-3] и использует один и тот же синтаксис как для пассивных, так и для активных объектов. Не понимая связи параллельных процессов с объектно-ориентированным программированием, трудно оценить все преимущества использования sC++. Чтобы прояснить эту связь, обратимся к объектно-ориентированной технологии OMT/Rumbaugh [4]. В соответствии с этим методом разработка приложений должна происходить в три фазы: анализ, проектирование и реализация. Во время фазы анализа разрабатываются три модели: объектная, динамическая и функциональная. Фаза проектирования опирается на эти модели.

Объектная модель определяет компоненты данных, уместные для конкретного приложения, их атрибуты и характерные для них операции. Динамическая модель определяет последовательность операций на основе диаграмм состояний. Функциональная модель использует диаграммы потоков данных для определения входных и выходных данных, источников и приемников данных, расположения мест передачи и приема данных. В сущности, объектная модель описывает данные, в то время как две другие модели описывают аспекты, связанные с параллельными процессами. Аспекты, включаемые в функциональную модель, связаны с передачей сообщений и параллельным выполнением.

Для описания параллельных процессов предлагаются и другие расширения объектно-ориентированных языков. Однако авторы этих решений обычно делают акцент на повышении скорости исполнения в случае применения параллельных процессов, а не на большей пригодности процессов для анализа, как в подходе с моделями.

Синтаксис и семантика sC++

Язык sC++ вводит концепцию активного объекта. Синтаксис определений, реализаций, ссылок, вызовов наследования и удаления активного объекта идентичен синтаксису стандартных — или пассивных — объектов. Активный объект может содержать внутреннюю активность, которая выполняется в параллель с активностью других аналогичных объектов. Эта активность может задержать выполнение методов объекта при их вызове извне, пока объект не будет готов принять их [7].

Рис.1. Три вызывающих объекта обращаются к

одному активному объекту. Первый объект

получает готовность выдать вызов, два других

становятся в очередь, пока обработка вызова не

будет завершена

На рис. 1 изображены три объекта, вызывающие метод send() из другого активного объекта. В этом случае первый объект, находящийся в состоянии готовности, может выдать вызов и выполнить метод. Если в это же время один или оба других объекта обращаются к send(), они становятся в очередь, связанную с методом, пока первый объект не закончит обработку вызова. Выполнение метода, принадлежащего активным объектам, неделимо и не пересекается с исполнением других методов того же самого объекта.

Внутренняя активность активного объекта выносится наружу методом, вызываемым телом, которое исполняется в принадлежащем объекту потоке. Это тело имеет то же имя, что и класс, но с префиксом «@», что во многом аналогично конструктору/деструктору. Если тело выполняется, методы блокируются. Они могут выполняться только в том случае, если тело заканчивает работу или если тело приостанавливает работу по оператору accept, содержащему имя вызываемого метода.

Рис.2. Два оператора вызов объекта и accept

выполняются во время рандеву. Один из них

блокируется, пока оба не будут полностью

обработаны
Например, на рис. 2 объект вызывает метод send(), который определяется в объекте obj1. Если obj1 прибывает в оператор accept send до того, как объект obj2 прибудет в оператор obj1.send(), объект obj1 блокируется, пока объект obj2 не вызовет send(). И наоборот, если объект obj2 прибывает в оператор obj1.send() до того, как объект примет send(), объект obj2 блокируется, пока объект obj2 не примет send(). Таким образом сочетание операторов вызова объекта и accept может идентифицироваться как рандеву, во время которого метод выполняется.

Один оператор accept может принять только один метод. Но часто требуется обеспечить выполнение нескольких методов одновременно, как в следующем примере.

1	State1:
2	select {
3		obj1.send(msg);
4		printf(«sent !
»);
5	| |
6		accept recv;
7		printf(«received !
»);
8	| |
9		waituntil (now()+100);
10		printf(«time-out !
»);
11		goto State2
12	 }

Оператор select включает три случая, каждый из которых отделен двойной вертикальной чертой. Каждый случай начинается с оператора-триггера: либо вызывающего, либо принимающего часть рандеву или тайм-аута (строки 3, 6 и 9).

Когда объект выполняет оператор select, он переходит в состояние ожидания, пока не появится одно из событий, определенных триггером. Первое же событие вызывает выполнение операторов, следующих за оператором-триггером, а затем программа выходит из оператора select. Этот оператор позднее должен быть выполнен повторно, если необходимо обработать и другие случаи.

Рис. 3. Часть автомата конечных

состояний, соответствующая примеру

кода в тексте

Функция now() из строки 9 возвращает значение счетчика, расположенного в ядре и отсчитывающего сотые доли секунд. Определенное оператором waituntil событие наступает, если значение счетчика превысит величину, переданную как параметр waituntil, и если за это время не наступило никаких других событий, определенных в операторе select.

Данный фрагмент программы соответствует автомату конечных состояний (Рис. 3).

Наследование и виртуальные методы

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

Наследование может обеспечить класс с различными интерфейсами, определенными его базовым классом и производными классами (созданными через подтипы). В языке sC++ каждый класс имеет дополнительное свойство активности (активный/пассивный) со следующими правилами наследования:

  • пассивный класс может наследовать другой пассивный класс в одном из режимов: личный, публичный, защищенный;
  • активный класс может наследовать другой активный класс в одном из режимов: личный, публичный, защищенный;
  • пассивный класс не может наследовать активный класс;
  • активный класс может наследовать пассивный класс только как личный.

Действительно, было бы небезопасно позволять пассивному классу наследовать активный класс, поскольку вызывающий объект мог бы получить его в активной форме, если класс использует создание подтипов. С другой стороны, включение активных и пассивных объектов как членов класса — в обеих категориях класса — не создает никаких проблем, поскольку имена членов всегда отражают категорию их принадлежности к классу. В довершение всего, программист может переопределить методы пассивного класса, который был унаследован как личный активным классом в публичном режиме. Действуя подобным образом программист может использовать синтаксис, уже установленный для пассивных объектов, например: BaseClass::method. Это свойство может быть использовано для введения пассивных методов в активные объекты.

Рис. 4. Наследование активного класса, в котором

каждый уровень класса имеет свое собственное

независимое тело

Наследование предохраняет тела активных базовых классов. Например, на рис. 4 показано, что каждый уровень класса имеет свое собственное независимое тело. Когда производный класс вызывает метод своего базового класса, производный класс синхронизируется, как и любой другой объект. Базовый класс должен принять метод, чтобы активизировать его выполнение. Когда внешний объект вызывает метод базового класса, создавая подтип, то для выполнения метода нужно принять только базовый класс, принятие производных классов не требуется.

Рис. 5. Поведение автомата

конечных состояний

на классе ActC1

Виртуальные методы, в терминах области видимости, встраивания и передачи параметров активных классов, определяются во многом подобно тому, как это делается в С++. Единственная разница в синхронизации: чтобы быть выполненным, виртуальный метод должен быть готов к работе во всех классах, в которых он был определен или переопределен. Другими словами, все другие методы (и тела) завершаются или находятся в состоянии приостановки на операторе, принимающем виртуальный метод. Таким образом, выполнение виртуального метода производит множественные рандеву между вызывающим объектом и всеми телами, которые принимают метод.

В следующем примере активный класс определяет методы a(), b() и c() со свойством виртуальности. Тело принимает методы в следующем порядке: а() или b() принимаются первыми (строки с 9 по 13), затем с() (строка 14), затем опять а() или b() и так далее. На рис. 5 показано состояние конечного автомата на этом классе ActC1.

1	active class ActC1 {
2 	public:
3		virtual void a() {printf («a
»); }
4		virtual void b() {}
5		virtual void c() {}
6	private:
7		@ActC1() {
8 			for (;;) {
9			  select {
10			      accept a;
11			| |
12			      accept b;
13			}
14			    accept c;
15			}
16		}
17	};

Благодаря ключевому слову virtual этот класс позволяет переопределить как свои действия, так и свои состояния. Например, следующий класс ActC1Deriv переопределяет методы b() и c() (строки 3 и 4), а также добавляет к их телам новые ограничения на то, как эти методы выполняются (строки с 9 по 15). Оба класса должны теперь одновременно принимать объект «b() соответственно с()», чтобы вычислить «b() соответственно с()».

Рис. 6. Для автомата

конечных состояний,

показанного на рис.5,

новое состояние

добавляется между

b() и x()
1	active class ActC1Deriv:public ActC1 {
2 	public:
3 		void b() {}	// redefinition
4 		void c() {}	// redefinition
5 		void x() {}	// new definition
6 	private:
7		@ActC1Deriv () {
8 			for (;;) {
9			     select {
10				accept c;
11			    | |
12				accept b;
13				accept x;
14				accept c;
15				}
16			}
17		}
18	 };

Новый класс добавляет новое состояние между b() и х() (Рис. 6), к состоянию, определяемом у классом ActC1. В соответствии с ActC1Deriv метод с() мог бы инициироваться повторно, но класс ActC1 навязывает включение в цикл либо метода а(), либо метода b(). Разумеется, изменить порядок состояний или удалить состояние, определенное базовым классом, невозможно.

Разработка программ в sC++

На примере задачи «обедающих философов» [8] видно, что sC++ может помочь в разработке параллельных приложений. Эта проблема (Рис. 7) часто используется для демонстрации сложности проектирования программ с конкуренцией за ресурсы.

Рис. 7. Иллюстрация проблемы

«Обедающих философов»

Целью является создание программы, которая моделирует поведение пяти философов, собравшихся за круглым столом. Каждый из философов некоторое время думает, затем ест, затем опять думает и так далее. Каждый из них ведет себя независимо от других, однако вилок запасено ровно столько, сколько философов, хотя для еды каждому из них нужно две. Таким образом, философы должны совместно использовать имеющиеся у них вилки (ресурсы). Когда один из философов хочет есть, он берет вилку слева от себя, если она в наличии, а затем — вилку справа от себя. Закончив есть, он возвращает обе вилки на свои места. Что произойдет, если все философы захотят есть в одно и то же время? Он будут находиться в состоянии ожидания второй вилки до бесконечности.

Приведем одно из решений. Закодируем вилки и поведение философов как объекты. В этом случае философы будут выступать как активные объекты, которые видят «появление» и «исчезновение» вилок. С точки зрения философов, вилки ведут себя по своему усмотрению, так что есть смысл и вилки определить как активные объекты. Поведение философов определяется в теле объекта (строки со 104 по 111), что отражает бесконечное повторение циклов еды и размышлений. Объект Fork имеет два метода, get() и put() (строки с 203 и 204), которые управляются телом Fork (строки с 203 по 209). В строке 114 определяется принадлежность к классу Philosopher пяти философов, а в строке 212 — принадлежность к классу Fork пяти вилок.

101	active class Philosopher {
102 	int i;		//номер философа
103	@ Philosopher() {	//тело
104	     for (;;) {
105		think: waituntil(now()+
random());
106		forks[i].get();
107		forks[(i+1)%5].get();
108		eat: waituntil(now()+
random());
109		forks[i].put();
110		forks[(i+1)%5].put();
111	     }
112	   }
113	};
114	Philosopher phil[5];

201	active class Fork{
202	public:
203		void get()  {}
204		void put()  {}
205		@Fork()  {	//тело
206		     for (;;)  {
207			accept get;
208			accept put;
209		     }
210		}
211	};
212 	Fork forks[5];

Программа моделирует время размышления философов введением задержки в строке 105. В строке 108 определяется период времени, в течение которого философ ест. Первый философ, пытающийся взять вилку (строка 106 или 107) получает ее — это результат исполнения метода get(). Это освобождает соответствующий оператор accept (строка 207), после чего выполняется оператор accept put в строке 208.

Второй философ, у которого проснулся аппетит, пытается получить ту же самую вилку, но находится в состоянии ожидания, поскольку метод get более не применим. Когда вилка возвращается на свое место, оператор accept put (строка 208) освобождается, и реактивируется метод get(), который незамедлительно выполняется, если есть вызов. В противном случае метод выполняется , когда другой философ вызовет этот метод.

Рис. 8. Два конечных автомата, соответствующие вилке и философу.

Надчеркнутые операторы представляют обращения, а все другие

операторы — ответы

В нашем случае второй философ и вилка могут быть представлены автоматами конечных состояний (Рис. 8). Символы в автомате состояний философа представляют собой обращения к методам, соответствующим вилке. Символы в автомате — вилке соответствуют принятию метода. Каждый автомат может перейти в другое состояние, если дополнительный символ готов. Например, Fi (get выполняется одновременно с переходом get в Fork[i]. Поскольку выполнение метода в sC++ неделимо, нет нужды рассматривать состояния внутри методов get() и put().

Рис. 9. Одиночный конечный автомат, который комбинирует две машины

с состояниями вилок и философов

На рис. 9 изображен конечный автомат, который комбинирует четыре состояния: два философа и две вилки. (Пример может быть модифицирован до любого числа философов и вилок.) Каждое состояние этого составного автомата включает по одному состоянию каждого простого автомата. Переход для составного автомата охватывает все возможные переходы из каждого простого состояния. Нижнее состояние в середине рис. 9 не имеет исходящих стрелок — это тупик.

Как же избежать тупиковых ситуаций? Второе решение проблемы «обедающих философов» позволяет это сделать и еще не требует каких-либо семафоров или специального инструментария для параллельных процессов. Идея заключается в том, чтобы заставить философа просить соседа одолжить ему вторую вилку. При этом вилка становится пассивным объектом, как это имеет место в следующем фрагменте программы:

101	active class Philosopher {
102		int i;
103		void please() { forks[i].get(); }
104 		void thankYou() {forks[i].put();}
105		@Philosopher() {		// body
106			for (;;) {
107		think:
108			...
109		wantToEat:
110			...
111		eat:
112			...
113		 	}
114		}
115	};
116	Philosopher phil[5];

201	class Fork{		// passive class
202	int available;
203	public:
204		void get() {available = false;}
205		void put() {available = true; }
206		Fork() {		// initialization
207			available = true;
208		}
209	};
210	Fork forks[5];

Эта программа предоставляет вилки с помощью переменных, которые показывают, когда вилки доступны (строка 202). Каждый философ теперь понимает новые запросы: please(строка 103) и thankYou(строка 104). Эти два метода могут быть использованы соседями для запрашивания вилок или их возвращения. Во время раздумий философ сознательно отдает вилку по требованию и затем снова запрашивает ее, если он готов есть. Следующие операторы выполняются во время раздумий философа:

1	think:
2		eatingTime = now() + random();
3		for (;;)
4 		    select {
5			    accept please;
6			| | 
7			    accept thankYou;
8			| |
9			    waituntil (eatingTime);
10			    goto wantToEat;
11		    }

В этом случае каждый философ вычисляет следующее время еды только один раз во время каждого периода размышлений (строка 2), так что время еды не меняется, даже если философ должен одалживать вилку несколько раз (строки 5 и 7). Когда наступает время еды, философ i входит в другой цикл:

1	wantToEat:
2		for (;;)
3		    select {
4			accept please;
5		    | |
6			accept thankYou;
7		    | |
8			when (forks[i].available)
9			    phil[(i+1)%5].please();
10			forks[i].get();
11			goto eat;
12		    }
13	}

Философ i может выйти из цикла только в том случае, если он готов выполнить операторы с 9 по 11, что в свою очередь возможно только если его вилка не была взята взаймы другим философом. Если его собственная вилка свободна, философ i ждет любого из трех событий, перечисленных в строках 4, 6 или 9. Если accept please (строка 4) вызывается соседом слева до того, как сосед справа примет вызов please (строка 9), собственная вилка философа исчезает, что устанавливает переменную наличия вилки в значение «ложь». Философ i теперь должен ждать возвращения вилки, что будет иметь место, когда взявший эту вилку философ вызовет метод thankYou i-того философа. Когда философ выходит из цикла, он может есть. Во время еды он игнорирует все запросы please и thankYou. Когда же он завершает свой обед, то кладет обе вилки на стол (строки 2 и 3) и начинает снова думать:

1	eat:	waituntil (now()+random());
2		forks[i].put();
3 		phil[(i+1)%5].thankYou();
Рис. 10. Конечный автомат, описывающий поведение философа.

Надчеркнутые операторы представляют обращения к методам, а

обычные операторы — принятие методов

Это решение еще может обеспечить безопасность методов, описанных для первого решения. Однако вилки больше не имеют программного состояния, они стали членами данных. Конечный автомат на рис. 10 представляет поведение философа в различных состояниях. Любая комбинация состояний этого конечного автомата не приводит автомат в тупик.

Активные объекты как альтернатива обратным связям

Многозадачность и обратные связи — главные альтернативы подхода, основанного на активных объектах. Концептуально многозадачность во многом подобна активным объектам, потому что активный объект является формой задачи, определяющей очень гибкие средства синхронизации: метод вызывает сам себя. Программирование с использованием обратных связей чаще всего применяется при разработке приложений с использованием графических пользовательских интерфейсов, архитектуры клиент-сервер и т.д. Очень важно сопоставить парадигму активного объекта с парадигмой обратной связи.

Рис. 11. Обратные связи, распределяемые в цикле

Рассмотрим рис. 11. Первая функция активизируется либо нажатием на клавишу, либо тайм-аутом. Вторая функция — данными, напечатанными в текстовом окне, третья — поступлением сообщения на сокет.

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

Как только интерфейс готов, генерируется исполняемый код, содержащий цикл обработки событий: ожидание следующего события, вызов соответствующей функции, а после возвращения из функции — ожидание следующего события. Информация о том, какая функция должна быть вызвана при наступлении события, обычно хранится в таблице. Когда в программе появляются новые кнопки или текстовые поля, имена соответствующих функций должны быть включены в главный цикл. Если функция имеет форму метода, включаемого в объект, то она должна иметь стандартную форму, известную главному циклу. При таком подходе программа создает кнопку, но кнопка вызывает функцию внутри программного объекта.

Многие с успехом используют эту парадигму. Однако не стоит полагаться на какой-либо метод формальных спецификаций. Несомненно, он может быть связан с концепцией конечных автоматов, но результирующий код в этом случае будет мало похож на спецификации, что сильно затрудняет отладку. Для преобразования решения проблемы обедающих философов в код с обратными связями потребовалось бы разбить решение на столько функций, сколько существует событийных шагов в поведении философов.

В sC++ кнопка определяется как активный объект, который содержит метод Pressed(). Данный метод может вызываться объектами, желающими опросить кнопку. Активный объект «кнопка» обнаруживает, когда пользователь нажмет кнопку на экране, и принимает метод Pressed(), в случае ее активации. Вызывающая процедура синхронизируется посредством рандеву.

Сокеты ТСР могут включаться в активные объекты тем же способом. Вызываемый для получения данных из сокета метод принимается объектом только тогда, когда некоторые данные передаются по сети.

Есть несколько преимуществ использования sC++ по сравнению с парадигмой обратной связи:

  • Порядок обработки событий ясно отражается в последовательности операторов.
  • sC++ допускает разбиение главного цикла обратной связи на несколько
  • объектов.
  • Программа как создает, так и читает объекты, которые генерируют события; объектам нет необходимости вызывать функции программы.
  • Расширение возможности повторного использования кода, графический интерфейс пользователя, драйверы сети, все управляемые событиями функции могут быть организованы как библиотеки активных объектов и, таким образом, нет нужды включать их в главный цикл.
  • Чтение текстового поля или сокета во многом подобно выполнению операторов scanf или read (C++).
  • Несколько программ могут читать с одного и того же устройства.
  • Программу легче моделировать и анализировать.

Полезность активных объектов не ограничивается приложениями, которые используют общую память — они также хорошо адаптированы к структуре распределенных приложений и для разработки межплатформенного ПО.

sC++ и промежуточное ПО

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

При вызове параметры запаковываются в сообщение и отправляются другому промежуточному объекту на процессоре сервера. Промежуточный объект декодирует сообщение, воссоздает параметры и вызывает сервер тем же способом, что и клиент. Промежуточный объект посылает возвращаемые значения назад клиенту тем же способом. Обычно одним процессором выполняется несколько серверов и клиентов одновременно, так что сообщения должны быть правильно адресованы. Для этого требуется мультиплексор, который может быть представлен как активный объект.

Используя sC++, автор применил описанную архитектуру с активными ТСР-объектами для создания многопоточной среды без каких либо дополнительных усилий. Кроме того, здесь можно было иметь несколько клиентов и серверов в одном и том же Unix. Имеется возможность использования Motif и взаимодействия с другими протоколами, а также хорошая поддержка моделирования. Несложной также оказалась адаптация компилятора IDL.

Эта разработка могла быть выполнена и в любой другой многопоточной среде. Однако использование объектов, начиная от проектирования приложения и кончая деталями промежуточного ПО, обеспечивает разработчиков однородной методологией, которая облегчает проектирование, отладку и применение. Например, удаленная программа может быть отлажена локально, а удаленная архитектура может быть создана позднее, на фазе применения.

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

Ресурсы sC++

Компилятор для sC++ базируется на компиляторе GNU C FSF. Разработчики sC++ рассчитывают на то, что расширение sC++ будет включено в одну из следующих версий стандартного компилятора FSF. Сегодня компилятор sC++ работает на платформах DEC-Alpha, Sun, SGI, Windows 95 и Linux. Общедоступны несколько библиотек: активный интерфейс для Motif, библиотеки сигналов и сокетов, а также аналитические и отладочные ядра. На языке sC++ уже написаны приложения, насчитывающие в совокупности несколько десятков тысяч строк кода.

Об авторе

Клод Птипьер (Claude Petitpierre) — профессор Федеральной политехнической школы Лозанны (EPFL). В сферу его научных интересов входят вопросы разработки программного обеспечения для коммуникаций, компьютерные сети.

Claude Petitpierre "Synchronous C + + : A Language for interactive applications", - IEEE Computer, October 1998, pp. 65-72. Reprinted with permission, Copyright IEEE CS, 1998, All rights reserved.


Литература

[1] R. Milner, Communication and Concurrency, Prentice Hall, Englewood Cliffs, N.J., 1989.

[2] R. Cleavland, J. Parrow, and B.Steffen, «The Concurency Workbench: A Semantics-Based Tool for the Verification of Concurrent Systems,» ACM Toplas, Jan. 1993, pp. 36-72.

[3] G.J. Holzmann, Design and Validation of Computer Protocols, Prentice Hall, Englewood Cliffs, N.J., 1991.

[4] J. Rumbaugh et al., OO Modelling and Design, Prentice Hall, Englewood Cliffs, N.J., 1991.

[5] G. Agha, P. Wegner, and A. Yonezawa, «Research Directions in Concurrent OO Programming», MIT Press, Cambridge, Mass., 1993.

[6] D. Caromel, «Toward a Method of OO Concurent Programming», Comm. ACM, Sept. 1993, pp. 90-116.

[7] C.A.R. Hoare, «Monitors: An Operating System Structuring Concept», Comm. ACM, Oct. 1974.

[8] E. W. Dijkstra, «Hierarchical Ordering of Sequential Processes,» Operating System Techniques, C.A.R. Hoare and R.H. Perrot, eds., Academic Press, New York, 1972.