Размышлизмы о выборе языка реализации.
Данная статья – лишь первая, робкая и, скорее всего, неудачная попытка обратить внимание сообщества QNX-программистов на один специфический вопрос – выбор инструмента реализации для создаваемых систем.
Задумка этой статьи родилась уже давно. Родилась она из многочисленных "около-" и "непосредственно-" программистских бесед в кругу харьковского сообщества кьюниксоидов и особенно - с Olej. Но автору для, хоть в малой степени связного оформления мыслей, не хватало (1) времени и (2) весомой моральной палки-погонялки, что бы, наконец, оторвать задницу от кресла на основной работе и засесть за OpenOffice. Наконец вторая причина вызрела просто-таки в жаренного петуха в виде морального долга взятого на себя обещания вести раздел по Аде и ругани Olej за "заплесневевшую" тему об объектах синхронизации... Ну, вообщем – что получилось... (первая-то причина не исчезла :о) )...
Так вот, несколько раз в ходе бесед, диалогов и дискуссий прорывалась тема заинтересованности Olej и меня в использовании Ады для разработок программ под QNX. Ада – один из языков, которым я питаю симпатию, как программист и всегда было желание попробовать его в качестве основного языка какого-нибудь проекта.
Сразу может возникнуть вопрос: зачем? – ведь есть прекрасные компиляторы С/С++ или другие языки. Но тут как раз в разговорах стала вырисовываться картина "коллективного видения", почему именно Ада.
Современные ОС располагают богатым набором примитивов для поддержки многозадачности и синхронизации выполнимых сущностей (задач, процессов, потоков). Доступ к этим средствам выполняется через некоторый API – либо на уровне вызова системных функций, либо через стандартизированные библиотеки. И такой подход присутствует практически во всех известных операционных системах и считается как бы сам собой разумеющимся. То есть это – устоявшаяся вещь.
Но! Как раз вот здесь и кроется проблема. Проблема, как мне кажется, в недостаточной формализации именно на уровне языка программирования тех понятий из области многозадачности, которые устоялись в программировании, как в дисциплине. Опять: «А зачем?» А затем, что формализация предусматривает подверженность систематизации, верификации и тестирования. Прекрасно, что в QNX есть всякие разные замечательные вещи, реализованные на системном уровне, но очень жаль, что нет языка, в котором эти понятия (может и названные по-другому) являются неотъемлемой органичной частью. Реализация на уровне API приветствуется, но это ведёт к ситуации «кто - в лес, кто - по дрова». Даже умельцы нестандартного использования появляются. Я сам люблю «извраты» и «хитровыделанные» находки и решения, но это не наш путь. Наш путь должен вести к написанию надёжного, хорошо читаемого (!), легко понимаемого, верифицируемого и тестируемого кода. Как этого достигнуть – сказано выше: пока на уровне языка реализации не появятся вкрапления поддержки многозадачности и синхронизации – о написании надёжного кода можно забыть... Или достигать его через страшный напряг по организационной линии (ввод стандартов в группах разработчиков, покупка тонн макулатуры и пр.) и написания дополнительных наборов сервисных утилит.
Естественно, что будут упрёки в «ущемлении свободы самовыражения». Кому не нравится – пишите на ассемблере – там свободы – через край.
На самом деле это не ущемление, а введение определённой культуры и дисциплины (прежде всего в организации мышления программиста и проектирования систем). Собственно, классики призывают к этому ещё со времён оных. На уровне алгоритмов и структур данных уже несогласных с этим нет, а вот с примитивами мультизадачности мы находимся примерно на уровне Си++, когда ООП в нём реализовывалось через препроцессор.
Посему, мне видится введение таких примитивов в языки реализации, естественным «продолжением банкета». То есть период реализации (использования) средств многозадачности (и всего, что с этим связано) через API нужно проходить как можно скорее или как можно активнее стараться, что бы он поскорее ушёл.
Дополнительный плюс заключается в повышении уровня мобильности создаваемых программ: пользователю предоставляется целостный механизм организации межзадачной связи, НЕ ОСНОВАННЫЙ НА ЯВНЫХ ОБРАЩЕНИЯХ К БАЗОВОЙ ОПЕРАЦИОННОЙ СИСТЕМЕ.
Кроме того, ещё один немаловажный момент: постепенное превращение императивных (!) языков из СРЕДСТВА КОДИРОВАНИЯ в СРЕДСТВО ПРОЕКТИРОВАНИЯ СИСТЕМЫ. Тут уж само по себе естественно выдвижение подобных требований.
Поэтому я и заинтересован в применении Ada и прочих современных высокоуровневых языках в реализации проектов на QNX.
Разработки здесь не прекращаются и дают прекрасные результаты.
К тому же, переходя на новый уровень осмысливания и вовлечения новых средств в языки, можно натолкнуться на неординарные решения в применении, казалось бы, устоявшихся понятий. Ниже я приведу примеры кода на трёх языках. Знания этих языков не требуется – примеры достаточно просты. В случае затруднения можно довольно легко отыскать описания языков в Сети. Примеры чисто иллюстративны и призваны показать реализацию базовых понятий многозадачности в этих трёх языках.
Так вот, на счёт "механизмов". Оказалось, что можно не наращивать количество примитивов синхронизации в Осях, а, в принципе, вообще без них обойтись. Например в Active Oberon (язык, далее - АО) и bluebottle (ОСь на его основе) нет ничего, кроме блоков (BEGIN ... END) помеченных, как EXCLUSIVE и оператора AWAIT(условие продолжения). Все остальные (воспринимаемые сообществом, как атомарные) примитивы выражаются через эти два средства.
Я
приведу несколько примеров выражения некоторых "атомарных"
средств синхронизации средствами АО.
Несколько предварительных
замечаний.
"*"
(звёздочка) после имени означает, что данное описание видимо
(экспортируется) за пределами модуля.
{EXCLUSIVE}
– данный блок может выполнять только один поток.
AWAIT(условие)
– если проверка дала ложь – мы стопорим поток и ставим
его в очередь потоков, ожидающих, когда условие станет истинным. Как
только другой поток (в другом методе данного экземпляра объекта!)
делает условие истинным – запускается первый поток, ожидавший
выполнения условия.
"&" перед именем метода класса
показывает, что этот метод вызывается при создании экземпляра (аналог
конструктора в других языках).
В остальном синтаксис похож на
синтаксис Оберона-2, (Оберона, Модулы-2, отчасти Паскаля).
TYPE
Signal*
= OBJECT
VAR
in: LONGINT; (*next ticket to
assign*)
out: LONGINT; (*next ticket to
service*)
(*
entries with (out <= ticket < in) must wait *)
PROCEDURE
Wait*;
VAR ticket: LONGINT;
BEGIN {EXCLUSIVE}
ticket := in; INC(in); AWAIT(ticket - out < 0)
END Wait;
PROCEDURE
Notify*;
BEGIN {EXCLUSIVE}
IF out # in THEN
INC(out) END
END Notify;
PROCEDURE
NotifyAll*;
BEGIN {EXCLUSIVE}
out := in
END
NotifyAll;
PROCEDURE
& Init;
BEGIN
in := 0; out := 0
END
Init;
END Signal;
Re-entrant
Locks
---------------------
Объект ReentrantLock позволяет
повторно захватывать некий объект владельцем этого объекта. Клиенты
этого объекта должны явно вызывать методы Lock и Unlock вместо
пометки их защищённых областей с помощью модификатора EXCLUSIVE.
ReentrantLock*
= OBJECT
VAR
lockedBy: PTR;
depth: LONGINT;
PROCEDURE
Lock*;
VAR me: PTR;
BEGIN {EXCLUSIVE}
me
:= AosActive.CurrentThread();
AWAIT((lockedBy =
NIL) OR (lockedBy = me));
lockedBy := me;
INC(depth)
END Lock;
PROCEDURE
Unlock*;
BEGIN {EXCLUSIVE}
DEC(depth);
IF depth = 0 THEN lockedBy := NIL END
END Unlock;
END
ReentrantLock;
Binary
and Generic Semaphores
-------------------------------------
Это
хорошо известный примитив, введённый Дейкстрой. Заметьте, что
возможность реализации семафоров, показывает, что модель Активного
Оберона также является примитивом синхронизации и достаточно мощной
для поддержки защиты и синхронизации параллельных задач (процессов).
MODULE
Semaphores;
TYPE
Sem*
= OBJECT (* Binary Semaphore *)
VAR taken: BOOLEAN
PROCEDURE
P*; (*enter semaphore*)
BEGIN {EXCLUSIVE}
AWAIT(~taken); taken := TRUE
END P;
PROCEDURE
V*; (*leave semaphore*)
BEGIN {EXCLUSIVE}
taken := FALSE
END V;
PROCEDURE
& Init;
BEGIN
taken := FALSE
END
Init;
END Sem;
GSem*
= OBJECT (* Generic Semaphore *)
VAR slots: LONGINT;
PROCEDURE
P*;
BEGIN {EXCLUSIVE}
AWAIT(slots > 0);
DEC(slots)
END P;
PROCEDURE
V*;
BEGIN {EXCLUSIVE}
INC(slots)
END V;
PROCEDURE
& Init(n: LONGINT);
BEGIN
slots := n END
Init;
END GSem;
END Semaphores.
Barrier
------------
Барьеры
используются для совместной синхронизации активностей. Если
активности (деятельности) определяются как:
Pi = Phasei,0; Phasei,1; ..... Phasei,n
То барьер используется для удостоверения того, что все активности выполнены Phase-i,j before starting Phasei,j+1. Код может может выглядеть примерно так:
FOR
j := 0 TO N DO
Phase(i, j); barrier.Enter
END;
MODULE
Barriers; (** prk/pjm 12.6.97 **)
(*
A barrier is used to synchronize N activities together. *)
TYPE
Barrier
= OBJECT
VAR n, N: LONGINT;
PROCEDURE
Enter*;
VAR i: LONGINT;
BEGIN {EXCLUSIVE}
i := n DIV N;
INC(n);
AWAIT (i < n DIV N)
END Enter;
PROCEDURE
& Init (nofProcs: LONGINT);
BEGIN
N :=
nofProcs; n := 0
END Init;
END Barrier;
END
Barriers.
MODULE
Buffers;
CONST BufLen = 256;
TYPE
(*
Buffer- First-in first-out buffer *)
Buffer*
= OBJECT
VAR
data: ARRAY BufLen OF
INTEGER;
in, out: LONGINT;
(* Put - insert
element into the buffer *)
PROCEDURE
Put* (i: INTEGER);
BEGIN {EXCLUSIVE}
AWAIT
((in + 1) MOD BufLen # out); (*AWAIT ~full *)
data[in] := i;
in := (in + 1) MOD BufLen
END
Put;
(*
Get - get element from the buffer *)
PROCEDURE Get* (VAR i:
INTEGER);
BEGIN {EXCLUSIVE}
AWAIT (in #
out); (*AWAIT ~empty *)
i := data[out];
out := (out + 1) MOD BufLen
END Get;
PROCEDURE
& Init;
BEGIN
in := 0; out := 0;
END
Init;
END Buffer;
END
Buffers.
По-моему – достаточно ясные примеры.
А как пример приложения (почти :о) ) приведём пример ЧИТАТЕЛИ-ПИСАТЕЛИ (вернее класс, реализующий такой подход к некоторому ресурсу).
Readers
– Writers
----------------------
Парадигма
Readers - Writers регулирует доступ к данным в критической секции.
Только один Писатель (Writer) (деятельность, изменяющая состояние объекта) или много Читателей (Readers) (деятельности, не изменяющие состояния объекта) могут находиться в одной и той же критической секции кода в одно и то же время.
MODULE
ReaderWriter;
TYPE
RW = OBJECT
(* n = 0, empty *)
(* n <
0, n Writers *)
(* n > 0, n Readers *)
VAR
n: LONGINT;
PROCEDURE
EnterReader*;
BEGIN {EXCLUSIVE}
AWAIT(n >=
0); INC(n)
END EnterReader;
PROCEDURE
ExitReader*;
BEGIN {EXCLUSIVE}
DEC(n)
END
ExitReader;
PROCEDURE
EnterWriter*;
BEGIN {EXCLUSIVE}
AWAIT(n =
0); DEC(n)
END EnterWriter;
PROCEDURE
ExitWriter*;
BEGIN {EXCLUSIVE}
INC(n)
END
ExitWriter;
PROCEDURE
& Init;
BEGIN
n := 0 END Init;
END
RW;
END
ReaderWriter.
В
Аде тоже присутствует поддержка многозадачности и синхронизации –
задачи и рандеву (ссылаются на Хоара, как на инициатора реализации
этого механизма в этом языке). Задачи могут быть представлены, как
аналоги процессов (потоков). Они предоставляют процедурный интерфейс
(как в пакетах), то есть обращение к ним синтаксически не отличается
от вызова процедуры. Положительным моментом отказа от классических
средств обеспечения синхронизации является осуществление всей
деятельности по взаимодействию и синхронизации на одном и том же
логическом уровне. Это просто обращение программных объектов друг к
другу, без скрытых механизмов (сигналы, сообщения).
Говоря по простому, рандеву – это момент, когда две задачи обмениваются данными друг с другом. В этот момент он входят в критическую секцию. Любая из двух взаимодействующих задач может первой перейти в состояние готовности к рандеву и будет ожидать прихода в это состояние второй задачи. Касательно QNX - ничего не напоминает? :о)
Реализация механизма взаимодействия задач в Аде ценна своим многообразием выразительных средств, с помощью которых можно описать широкий спектр разных случаев взаимовызовов. Ниже я опишу несколько таких типичных «паттернов».
Предположим, у нас есть две задачи: А и В. Рассмотрим как каждая из них может вести себя во время их взаимодействия...
Сначала – вызывающая сторона: Нам нужно, находясь в А, вызвать вход REQUEST задачи В.
Безусловный вызов (синхронный?).
Тут всё понятно. Вызываем и если попадаем на занятую задачу – блокируемся и ждём, пока не начнётся рандеву.
...
B.REQUEST
...
Условный вызов:
Здесь, если вызываемая задача чем-то занята – делаем что-то другое.
...
select
B.REQUEST;
else
ALTERNATIVE_ACTIONS;
end
select;
...
Временной вызов:
Здесь – то же, что и в предыдущем пункте, но только ожидаем освобождение В течение Т, а потом выполняем ALTERNATIVE_ACTIONS.
...
select
B.REQUEST;
or
delay T;
ALTERNATIVE_ACTIONS;
end select;
...
Теперь – типичные случаи на приёмной стороне... Только сейчас entry_1 и entry_2 – это входы задачи B.
Места, где отмечены критические секции и есть моменты рандеву (do ... end;). Во время их выполнения вызывающая задача блокируется.
Фиксированный порядок просмотра входов на предмет не запрашивает ли кто их вызова.
Здесь приоритет по началу рандеву получает вход entry_1
...
select
accept entry_1; do ... end; -- критическая
секция
OTHER_A_PROCESSING;
accept
entry_2; do ... end; -- критическая секция
OTHER_B_PROCESSING;
end
select;
...
Выборочное ожидание по множеству входов (в порядке поступления)
Смотрим на какой из входов приходит вызов, принимаем, остальные входы блокируются. Если Вызовы приходят одновременно, то выбор входа происходит случайным образом.
...
select
accept entry_1; do ... end; -- критическая
секция
OTHER_A_PROCESSING;
or
accept entry_2; do ... end; -- критическая секция
OTHER_B_PROCESSING;
end
select;
...
Обусловленный порядок (условный приём вызовов).
Здесь, ОТКРЫТЫМИ (рассматриваемыми в качестве входов, по которым возможно начало рандеву) будут входы, для которых выражения после when (X, Y) будут истинными.
...
select
when X =>
accept
entry_1; do ... end; -- критическая секция
OTHER_A_PROCESSING;
or
when Y =>
accept
entry_2; do ... end; -- критическая секция
OTHER_B_PROCESSING;
end
select;
...
С выделением лимита времени.
Здесь полезна аналогия, по которой, если в течение времени Т не происходит вызова входов А или В, то вход Т вызывается системой... :о)
...
select
accept entry_1; do ... end; -- критическая
секция
OTHER_A_PROCESSING;
or
accept entry_2; do ... end; -- критическая секция
OTHER_B_PROCESSING;
or
delay T;
DELAY_PROCESSING;
end select;
...
В качестве примера – те же ЧИТАТЕЛИ-ПИСАТЕЛИ.
procedure
MAIN is
task type READERS;
task type WRITERS;
task SHARED_DATA is
entry START_READ (SR: out integer);
entry STOP_READ;
entry START_WRITE (SW: in integer);
end
SHARED_DATA;
reader_1,
reader_2, ... , reader_N: integer; -- задачи-читатели
writer_1, writer_2, ... , writer_N: integer; --
задачи-писатели
task body READERS is
READ_ITEM: integer;
begin
loop
...
SHARED_DATA.START_READ(READ_ITEM);
SHARED_DATA.STOP_READ;
... -- тут мы как-то работаем с
полученным значением
end
loop;
end READERS;
task body WRITERS is
WRITE_ITEM: integer;
begin
loop
... -- тут мы как-то получаем
READ_ITEM для
передаче его в разделяемый ресурс
SHARED_DATA.START_WRITE(WRITE_ITEM);
...
end loop;
end WRITERS;
task body SHARED_DATA is
ITEM: integer;
NO_READERS: integer := 0;
begin
accept START_WRITE (SW: in integer)
do
ITEM := SW;
end
START_WRITE;
loop
select
when
START_WRITE’COUNT = 0 =>
accept
START_READ (SR: out integer)
do
SR := ITEM;
end
START_READ;
NO_READERS := NO_READERS +
1;
or
accept
STOP_READ;
NO_READERS := NO_READERS – 1;
or
when NO_READERS = 0 =>
accept START_WRITE (SW: in integer)
do
ITEM := SW;
end
START_WRITE;
loop
select
accept START_READ (SR: out integer)
do
SR := ITEM;
end START_READ;
NO_READERS := NO_READERS + 1;
else
exit;
end select;
end loop;
or
terminate;
end select;
... -- тут мы как-то получаем
READ_ITEM для
передаче его в разделяемый ресурс
SHARED_DATA.START_WRITE(WRITE_ITEM);
...
end
loop;
end SHARED_DATA
begin
NULL;
End MAIN;
Ну
и под конец – то, что я воспринял как бомбу. Просто иначе я не
могу назвать то, о чём я прочитал две недели назад. Виртовская школа
родила ещё один язык в семействе Паскаль-Модула-Оберон. Называется он
Zonnon. В чём его «бомбистость»?
Основан он на Active Oberon, то есть наследует те же возможности активных объектов. Но добавлен ряд средств, про которые не часто услышишь при разговоре о ЯП.
Первое – это КАНАЛЫ. Впервые (на моей памяти) явно появились в языке параллельного программирования occam (ОККАМ).
Как это выглядит?
Вот так можно отправить какое-либо значение активности объекта:
a
! pi*x/180.0 (*
конвертируем радианы в градусы и отправляем значение по ссылке a
*)
! “29 Aug 2004” (*
послать строку активности объекта, что нас вызвала *)
Есть операторы и для приёма (свяжите с двумя вышеприведёнными строками примеров):
?
angle (* получить значение угла от
вызывающей активности *)
a ? date
(* получить строку с датой от вызываемой активности*)
На самом деле их следовало бы перегруппировать по активностям (вызывающая и вызываемая), потому, что в вызывающей (посылающей) активности мы всегда знаем ссылку на вызываемую, а вот вызываемая активность никогда не знает (без приложения усилий :о) ), кто её вызвал. Это похоже на взаимоотношения клиент-сервер:
a
! pi*x/180.0 (*
конвертируем радианы в градусы и отправляем значение по ссылке a
*)
a ? date (*
получить строку с датой от вызываемой активности*)
?
angle (* получить значение угла от
вызывающей активности *)
! “29 Aug
2004” (* послать строку активности объекта, что нас вызвала *)
А теперь, собственно, БОМБА.
ОПРЕДЕЛЕНИЕ АКТИВНОСТИ МОЖЕТ ВКЛЮЧАТЬ EBNF-СПЕЦИФИКАЦИЮ ПРОТОКОЛА ВЗАИМОДЕЙСТВИЯ КАК МОДИФИКАТОР ТИПА ПЕРЕЧИСЛЕНИЯ, ОПРЕДЕЛЯЮЩЕГО АЛФАВИТ ТЕРМИНАЛЬНЫХ СИМВОЛОВ СИНТАКСИСА ПРОТОКОЛА (рекомендую не вдаваться особо в смысл – из примера всё станет ясно :о) ). Имя активности и имя её перечислимого типа составляют сигнатуру активности.
Пример:
DEFINITION
Fighter;
ACTIVITY
Karate;
TYPE (* синтаксис протокола, в данном
случае допустима РЕКУРСИЯ *)
{
fight = { attack ( { defense attack } | RUNAWAY
[?CHASE] | KO | fight }.
attack = ATTACK strike.
defense = DEFENSE strike.
strike = bodypart
[ strangth ].
bodypart = LEG | NECK | HEAD.
strength =
integer.
} Keywords = (RUNAWAY, CHASE, KO, ATTACK, DEFENSE, LEG, NECK,
HEAD);
END Karate;
END Fighter.
OBJECT
Opponent IMPLEMENTS Fighter;
ACTIVITY Karate
IMPLEMENTS Fighter.Karate;
VAR t: OBJECT;
PROCEDURE fight;
BEGIN
WHILE t IS
Fighter.Karate.Keywords.ATACK DO
?t;
WHILE t IS Fighter.Karate.Keywords.DEFENSE DO
?t;
strike
IF t IS
Fighter.Karate.Keywords.ATACK THEN
strike
ELSE
HALT(13)
END;
END;
IF t
IS Fighter.Karate.Keywords.RUNAWAY THEN
IF не
истощён THEN !CHASE END;
RETURN;
ELSIF t IS
Fighter.Karate.Keywords.KO THEN RETURN
ELSIF t IS Fighter.Karate.Keywords.ATTACK THEN
fight
ELSE HALT(13); (*
ошибка протокола *)
END;
END (* while *)
END fight;
PROCEDURE strike;
BEGIN
IF t IS Fighter.Karate.Keywords.LEG OR
t IS
Fighter.Karate.Keywords.NECK OR
t IS Fighter.Karate.Keywords.HEAD
THEN
?t;
IF t
IS INTEGER THEN ?t END;
END
END strike;
BEGIN
?t;
fight;
END Karate;
END
Opponent.
OBJECT
Challenger;
IMPORT Opponent, Fighter;
VAR
opp: Opponent;
fk: Fighter.Karate;
...
opp := NEW Opponent; (* создаём противника
*)
fk := NEW
opp.Fighter.Karate; (* стратуем «диалог»
*)
fk ! Fighter.Karate.Keywords.ATACK
(* драться в соответствии с нашим протоколом *)
...
END Challenger.
Что это такое и чем это ценно? Вы определяете протокол, задаёте терминальные символы, на которых он строится, и реализуете более высокоуровневые элементы протокола. А компилятор проследит, что бы вы полностью доопределили и реализовали (в соответствии с заданными вами же правилами) элементы протокола. Это очень близко к системам, в которых разработчики реализуют конечные автоматы, но там вы сами строите систему поддержки выбранного вами подхода, а здесь вам помогает компилятор.
Некоторые
сравнения и выводы.
Каждый из приведённых выше языков имеет свои достоинства и недостатки. АО показывает, что можно построить логичные, «более атомарные», но находящиеся на сравнительно высоком уровне абстракции элементы параллельности и синхронизации. Он обходится естественными для большинства программистов (да и людей вообще) понятиями последовательности действий (BEGIN ... END) и оператора AWAIT ожидания выполнения логического условия. Однако, АО вводит понятие ИСКЛЮЧИТЕЛЬНОСТИ – невозможности выполнить эту последовательность действий, одновременно двумя активными сущностями. Кроме того (наверное, впервые) действия в языке - не самостоятельные понятия, а привязаны к объектам. Собственно, объекты, наконец явно могут выражать понятия "активные агенты" и стать ПО-НАСТОЯЩЕМУ объектами, а не субъектами, как это было до сих пор... Это, в свою очередь, разгружает понятийную область от таких, достаточно искусственных понятий, как "процесс", "поток", «локальная память процесса (потока)», "взаимодействие между потоками и/или процессами". Программист довольно естественно переносит понятия объектов и ИХ активности (из предметной области) в понятия языка реализации. Упрощаются модели, их верификация и тестирование. Достаточно сказать, что в ОС bluebottle уже действительно достигнута единообразность построения архитектуры операционной системы снизу до верху – от самых примитивных механизмов поддержки многозадачности и драйверов и до целых подсистем. Недостатки АО видны в сравнении с Адой, где понятие параллельного процесса, выраженное через задачи является понятием уровня языка. В Аду были внесены достаточно мощные механизмы и подходы для реализации большинства типовых ситуаций взаимодействия между процессами (в их классическом понимании), что может служить огромным преимуществом при написании приложений на системах, подобных QNX. В подобных системах атомарность средств взаимодействия между процессами может приводить к большому пространству вариантов такого взаимодействия и просто к проектированию неудачных протоколов между объектами. Средствами же Ады такое пространство суживается до достаточно ограниченного, но, тем не менее, мощного множества, к тому же находящегося по «патронажем» синтаксиса и семантики Ады. Для реализации подобных вещей на АО, нам придётся проектировать новые объекты-носители понятий таких как: "протокол", объекты, реализующие таймеры ожидания ответной реакции – в общем, некий набор интерфейсных объектов.
Ну и наконец, на настоящий момент времени и на мой, прямо скажем, пристрастный взгляд всех этих недостатков лишён Зоннон. По крайней мере, с первого взгляда на его стартовую спецификацию. Здесь мы идём дальше Ады – нам позволено определять сам протокол взаимодействия формализованными средствами. Это расширяет пространство решений, но сохраняет верифицируемость принятых решений, не перегружая язык.
Из уже устоявшихся «бубенцов и флажков» и в Аде и в Зонноне имеется перегрузка операторов (кстати, в Аде она с самого начала и задолго до Си++). Во всех языках используется очень строгий синтаксис и правила приведения (или, вернее, ограничения по приведению) типов. В Аде и Зонноне реализован механизм обработки исключений (по именам, а не по типам, как в Си++). В АО и Зонноне реализована сборка мусора. В АО и Зонноне есть возможность управлять приоритетами «процессов». Честно говоря, я знаком только с фиксированным объявлением «приоритета» задачи в Аде (директива в исходном коде, во время компиляции).
Итак, видится достаточно разумным попытаться внедрить в практику языки последнего поколения. Какой выбрать?
АО – язык, ориентированный на собственную ОС. Именно там эта спарка (АО+bluebottle) показывает максимальный эффект и преимущества. Реализованы на платформах x86 и StrongARM. Всё же это больше язык-ОСь со своими глубоко взаимно проникающими механизмами и принципами (взять хотя бы сборку мусора на уровне ОС). К тому же, насколько мне известно, АО служит "экспериментальным" стендом для Зоннона и это достаточно "лабораторые" язык и система (по крайней мере, кроме ETHZ, мне не известна ни одна фирма, выполняющая заказы по реализации систем на этой "спарке"). Есть большая вероятность, что АО со временем "отомрёт", уступив дорогу своему "младшему брату" – Зоннону. Кроме того, что ETHZ выставляет на этот язык, как на очередную "веху", итог работ последнего десятилетия, он станет одним из основных языков на платформе dotNET. Из личных контактов с автором языка я понял, что проектировщики КРАЙНЕ ЗАИНТЕРЕСОВАНЫ в портировании и реализации Зоннона на других системах. Но – когда это будет? К тому же Зоннон развивается на деньги MS Research, а не мне вам рассказывать "степень заинтересованности" Билли в развитии не-Windows-систем... :о))) Итак, Зоннон тоже отпадает (хотя я посоветовал бы постоянно держать в поле зрения его развитие).
У нас остаётся только Ада. Дополнительным плюсом в её пользу является достаточно большой срок её реального использования, скрупулезная стандартизация, выпуск стандарта Ада95 и появление GNAT. Собственно последний довод перевешивает всё остальное... :о)
Ещё раз повторюсь. Всё написанное выше – не сравнение «что лучше». И сравнение и описания выше призваны показать преимущества применения высокоуровневых языков, а использование низкоуровневых средств ОС напрямую. Параллельность и синхронизация – ответственные и, подчас, слишком сложные вещи, что бы их оставлять «на откуп программисту» :о). Применяя же языки, в которых эти понятия реализованы (да к тому же языки со строгой типизацией!), мы получаем дополнительную гарантию создания более надёжных и переносимых систем.
Теме Ады будет посвящено несколько статей. В следующей я постараюсь раскрыть преимущества применения языков, подобных Аде по сравнению с Си++ и Явой. Сравнения будут проведены как на уровне синтаксических конструкций, так и на уровне семантики, заложенной в эти языки.