Размышлизмы о выборе языка реализации.

Владимир Лось (Wlad)
wwlos@hotmail.com
Редакция 1.05 от 14.01.2004

Данная статья – лишь первая, робкая и, скорее всего, неудачная попытка обратить внимание сообщества 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, отчасти Паскаля).

Signals
-------------
Signal реализует примитивы сигналов (примерно, как в Java и Modula-2). Он использует несколько изменённый «билетный алгоритм». Как и во многих магазинах, каждый клиент получает пронумерованную квитанцию, для гарантии обслуживания в порядке появления в магазине. По сути дела работают обёртки над операциями с индексами in и out.

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.

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

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. Собственно последний довод перевешивает всё остальное... :о)

Ещё раз повторюсь. Всё написанное выше – не сравнение «что лучше». И сравнение и описания выше призваны показать преимущества применения высокоуровневых языков, а использование низкоуровневых средств ОС напрямую. Параллельность и синхронизация – ответственные и, подчас, слишком сложные вещи, что бы их оставлять «на откуп программисту» :о). Применяя же языки, в которых эти понятия реализованы (да к тому же языки со строгой типизацией!), мы получаем дополнительную гарантию создания более надёжных и переносимых систем.

Теме Ады будет посвящено несколько статей. В следующей я постараюсь раскрыть преимущества применения языков, подобных Аде по сравнению с Си++ и Явой. Сравнения будут проведены как на уровне синтаксических конструкций, так и на уровне семантики, заложенной в эти языки.