Опубликован: 23.10.2005 | Доступ: свободный | Студентов: 4089 / 201 | Оценка: 4.44 / 4.19 | Длительность: 33:04:00
Специальности: Программист
Лекция 17:

От Simula к Java и далее: основные ОО-языки и окружения

Пример

Следующие фрагменты классов показывают общий колорит Simula. Они соответствуют классам системы управления панелями, проектирование которой рассмотрено в "Образец проектирования: многопанельные интерактивные системы" .

class STATE;
    virtual:
        procedure display;
        procedure read;
        boolean procedure correct;
    procedure message;
    procedure process;
begin
    ref (ANSWER) user_answer; integer choice;
    procedure execute; begin
            boolean ok;
        ok := false;
        while not ok do begin
            display; read; ok := correct;
            if not ok then message (a)
        end while;
        process;
    end execute
end STATE;
class APPLICATION (n, m);
        integer n, m;
begin
    ref (STATE) array transition (1:n, 0:m-1);
    ref (STATE) array associated_state (1:n);
    integer initial;
    procedure execute; begin
            integer st_number;
        st_number := initial;
        while st_number /= 0 do begin
                ref (STATE) st;
            st := associated_state (st_number); st.execute;
            st_number := transition (st_number, st.choice)
        end while
    end execute
    ...
end APPLICATION

Концепции сопрограмм

Наряду с базисными ОО-механизмами язык Simula предлагает интересное понятие - сопрограмма.

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

from some_initialization loop forever
    "Получить файл для печати"; "Напечатать его"
end

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

Сопрограммы спроектированы подобным же образом, но для выполнения в одном потоке управления. (Последовательная эмуляция параллельного выполнения называется квази-параллелизмом.) Сопрограмма прерывает свое собственное выполнение и предлагает продолжить выполнение (resume) другой сопрограмме в ее последней точке прерывания; прерванная сопрограмма позже может продолжиться сама.

Последовательное выполнение сопрограмм

Рис. 17.1. Последовательное выполнение сопрограмм

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

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

while continuation_condition do begin
    ... Действия ...;
    resume other_coroutine;
    ...Действия ...
end

Для некоторых сопрограмм условием continuation_condition часто является True, что эквивалентно бесконечному процессу (несмотря на то, что хотя бы одна сопрограмма должна завершиться).

Система, основанная на сопрограммах, обычно имеет основную программу, сначала создающую ряд объектов - сопрограмм, а затем продолжающую одну из них:

corout1 :- new C1; corout2 :- new C2; ...
resume corouti

Каждое выражение new создает объект и приступает к выполнению его тела. Но квазипараллельная природа сопрограмм (в отличие от истинного параллелизма процессов) поднимает проблему инициализации. Для процессов каждое new порождает новый процесс, запускает его, возвращая тут же управление исходному процессу. Но здесь только одна сопрограмма может быть активной. Если выражение new запустило основной алгоритм сопрограммы, то исходный процесс не получит вновь управление - у него не будет возможности создать C2 после порождения C1.

Simula решает эту проблему посредством инструкции detach. Сопрограмма может выполнить detach, возвращая управление блоку, создавшему его посредством new. Тела сопрограмм почти всегда начинаются с detach (если необходимо, после инструкции инициализации), а дальше обычно следует цикл. После выполнения своего detach сопрограмма приостановится до тех пор, пока главная, или другая, сопрограмма не продолжит ее выполнение.

Пример сопрограммы

Приведем пример некоторой ситуации, где сопрограммы могут оказаться полезными. Вам предлагается напечатать последовательность действительных чисел в качестве ввода, но каждое восьмое число в выводе нужно опустить. Вывод должен представлять собой последовательность строк из шести чисел (кроме последней строки, если для ее заполнения чисел не хватает). Если in обозначает n -й элемент ввода, вывод выглядит так:

i1    i2    i3    i4    i5    i6
i7    i9    i10    i11    i12    i13
i14    i15    i17    и т. д.

Наконец, вывод должен включать только 1000 чисел.

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

Введем три сопрограммы: producer (ввод), printer (вывод) и controller. Общая структура такова:

begin
    class PRODUCER begin ... См. далее ... end PRODUCER;
    class PRINTER begin ... См. далее ... end PRINTER;
    class CONTROLLER begin ... См. далее ... end CONTROLLER;
    ref (PRODUCER) producer; ref (PRINTER) printer;
     ref (CONTROLLER) controller;
    producer :- new PRODUCER; printer :- new PRINTER;
    controller :- new CONTROLLER;
    resume controller
end

Это главная программа, в обычном смысле этого слова. Она создает экземпляр каждого из трех классов - соответствующую сопрограмму и продолжает одну из них - контроллер. Классы приведены далее:

class CONTROLLER; begin
        integer i;
    detach;
    for i := 1 step 1 until 1000 do resume printer
end CONTROLLER;
class PRINTER; begin
        integer i;
    detach;
    while true do
        for i := 1 step 1 until 8 do begin
            resume producer;
            outreal (producer.last_input);
            resume controller
        end;
        next_line
    end
end PRINTER;
class PRODUCER; begin
        integer i; real last_input, discarded;
    detach;
    while true do begin
        for i := 1 step 1 until 6 do begin
            last_input := inreal; resume printer
        end;
        discarded := inreal
    end
end PRODUCER;

Тело каждого класса начинается с detach, что позволяет главной программе продолжать инициализацию других сопрограмм. Функция inreal возвращает число, прочитанное из входного потока, процедура outreal его печатает, процедура next_line обеспечивает переход на следующую строку ввода.

Сопрограммы хорошо соответствуют другим понятиям ОО-построения ПО. Заметим, насколько децентрализована приведенная схема: каждый процесс занимается своим делом, вмешательство других ограничено. Producer заботится о создании элементов ввода, printer - о выводе, controller - о том, когда начинать и заканчивать. Как обычно, хорошей проверкой качества решения является простота расширения и модификации; здесь явно надо добавить сопрограмму, проверяющую конец ввода (как просит одно из упражнений). Сопрограммы расширяют децентрализацию еще на один шаг, что является признаком хорошей ОО-архитектуры.

Архитектуру можно сделать еще более децентрализованной. В частности, процессы в описанной структуре должны все же активизировать друг друга по имени. В идеале им не нужно ничего знать друг о друге, кроме передаваемой информации (например, принтер получает last_input от producer ). Примитивы моделирования, изучаемые далее, позволяют это. После этого решение может использовать полный механизм параллелизма, описанный в одной из лекций. Его независимость от платформы означает, что он будет работать для сопрограмм, так же как истинный параллелизм.

Последовательное выполнение и наследование

Даже если класс Simula не использует механизмы сопрограмм (detach, resume), он помимо компонентов имеет тело (последовательность инструкций) и может вести себя как процесс в дополнение к своей обычной роли реализации АТД. В сочетании с наследованием это свойство ведет к более простой версии того, что в обсуждении параллелизма называлось аномалией наследования. Язык Simula, благодаря ограничениям (наследование единичное, а не множественное; сопрограммы, а не полный параллелизм), способен обеспечить языковое решение проблемы аномалии.

Пусть bodyC - это последовательность инструкций, объявленная как тело C, а actual_bodyC - последовательность инструкций, выполняемая при создании каждого экземпляра C. Если у C нет предка, actual_bodyC - это просто bodyC. Если у C есть родитель A (один, поскольку наследование одиночное), то actual_bodyC - по умолчанию имеет вид:

actual_bodyA; bodyC

Другими словами, тела предков выполняются в порядке наследования. Но эти действия по умолчанию, возможно, не то, что нужно. Для изменения порядка действий, заданных по умолчанию, Simula предлагает инструкцию inner, обозначающую подстановку тела наследника в нужное место тела родителя. Тогда действия по умолчанию эквивалентны тому, что inner стоит в конце тела предка. В общем случае тело A выглядит так:

instructions1; inner; instructions2

Тогда, если предположить, что само A не имеет предка, actual_bodyC имеет вид:

instructions1; bodyC; instructions2

Хотя причины введения подобной семантики ясны, соглашение выглядит довольно неуклюже:

  • во многих случаях потомкам необходимо создать свои экземпляры не так, как их предкам (вспомните POLYGON и RECTANGLE );
  • тела родителей и потомков, как, например C, становится трудно понять: прочтение bodyC еще ничего не говорит о том, что будет делаться при выполнении new ;
  • соглашение не переносится естественным образом на множественное наследование (хотя это не прямая забота Simula).

Трудности с inner - типичное следствие активности объектов, о чем говорилось при обсуждении параллелизма.

Почти все ОО-языки после Simula отказались от соглашения inner и рассматривали инициализацию объекта как процедуру.