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

Параллельность, распределенность, клиент-сервер и Интернет

Примеры

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

Обедающие философы

Блюдо спагетти обедающих философов

Рис. 12.11. Блюдо спагетти обедающих философов

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

Следующий класс описывает поведение философов. Благодаря механизму резервирования объектов с помощью сепаратных аргументов в нем, по существу, отсутствует явный код синхронизации (в отличие от обычно предлагаемых в литературе решений):

separate class PHILOSOPHER creation
    make
inherit
    GENERAL_PHILOSOPHER
    PROCESS
        rename setup as getup undefine getup end
feature {BUTLER}
    step is
            -- Выполнение задач философа
        do
            think
            eat (left, right)
        end
feature {NONE}
    eat (l, r: separate FORK) is
            -- Обедать, захватив вилки l и r
        do ... end
end

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

Простота этого решения объясняется способностью резервировать несколько сепаратных аргументов в одном вызове, здесь это left и right. Если бы мы ограничили число сепаратных аргументов в вызове одним, то в решении пришлось бы использовать один из многих опубликованных алгоритмов для захвата двух вилок без блокировки.

Главная процедура класса PHILOSOPHER не приведена выше, поскольку она приходит из класса поведения PROCESS: это процедура live, которая по определению в PROCESS просто выполняет from setup until over loop step end, поэтому все, что требуется переопределить, это step. Надеюсь, вам понравилось переименование обозначения начальной операции философа setup в getup.

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

Чтобы избежать смешения жанров, независящие от параллельности компоненты собраны в классе GENERAL_PHILOSOPHER:

class GENERAL_PHILOSOPHER creation
    make
feature -- Initialization
    make (l, r: separate FORK) is
            -- Задать l как левую, а r как правую вилки
        do
            left := l; right := r
        end
feature {NONE} -- Implementation
    left, right: separate FORK
            -- Две требуемые вилки
    getup is
            -- Выполнить необходимую инициализацию
        do ... end
    think is
            -- Любое подходящие действие или его отсутствие
        do ... end
end

Остальная часть системы относится к инициализации и включает описания вспомогательных абстракций. У вилок никаких специальных свойств нет:

class FORK end
Класс BUTLER ("дворецкий") используется для настройки и начала сессии:
class BUTLER creation
    make
feature
    count: INTEGER
        -- Число философов и вилок
    launch is
            -- Начало полной сессии
        local
            i: INTEGER
        do
            from i := 1 until i > count loop
                launch_one (participants @ i); i := i + 1
            end
        end
feature {NONE}
    launch_one (p: PHILOSOPHER) is
            -- Позволяет начать актуальную жизнь одному философу
        do
            p.live
        end
    participants: ARRAY [PHILOSOPHER]
    cutlery: ARRAY [FORK]
feature {NONE} -- Initialization
    make (n: INTEGER) is
            -- Инициализация сессии с n философами
        require
            n >= 0
        do
            count := n
            create participants.make (1, count); create cutlery.make (1, count)
            make_philosophers
        ensure
            count = n
        end
            make_philosophers is
            -- Настройка философов
        local
            i: INTEGER; p: PHILOSOPHER; left, right: FORK
        do
            from i := 1 until i > count loop
                p := philosophers @ i
                left := cutlery @ i
                right := cutlery @ ((i \\ count) + 1
                create p.make (left, right)
                i := i + 1
            end
        end
invariant
    count >= 0; participants.count = count; cutlery.count = count
end

Обратите внимание, launch и launch_one, используя образец, обсужденный при введении ожидания по необходимости, основаны на том, что вызов p.live не приведет к ожиданию, допуская обработку следующего философа в цикле.

Полное использование параллелизма оборудования

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

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

Рассмотрим сначала набросок класса, без параллелизма:

class BINARY_TREE [G] feature
    left, right: BINARY_TREE [G]
    ... Другие компоненты ...
    nodes: INTEGER is
            -- Число вершин в данном дереве
        do
            Result := node_count (left) + node_count (right) + 1
        end
feature {NONE}
    node_count (b: BINARY_TREE [G]): INTEGER is
            -- Число вершин в b
        do
            if b /= Void then Result := b.nodes end
        end
end

Функция nodes использует рекурсию для вычисления числа вершин в дереве. Эта косвенная рекурсия проходит через вызовы node_count.

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

separate class BINARY_TREE1 [G] feature
    left, right: BINARY_TREE1 [G]
    ... Другие компоненты ...
    nodes: INTEGER
    update_nodes is
            -- Модифицировать nodes, подсчитав число вершин дерева
        do
            nodes := 1
            compute_nodes (left); compute_nodes (right)
            adjust_nodes (left); adjust_nodes (right)
        end
feature {NONE}
    compute_nodes (b: BINARY_TREE1 [G]) is
            -- Модифицировать информацию о числе вершин в b
        do
            if b /= Void then
                b.update_nodes
            end
        end
    adjust_nodes (b: BINARY_TREE1 [G]) is
            -- Добавить число вершин в b
        do
            if b /= Void then nodes := nodes + b.nodes end
        end
end

В этом случае рекурсивные вызовы compute_nodes будут запускаться параллельно. Операция сложения будет ждать, пока не завершатся два параллельных вычисления.

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

Наличие двух проверок пустоты b может показаться неприятным. Однако это требуется для отделения распараллеливаемой части - вызовы процедур, запускаемых параллельно на left и right, - от сложений, которые по своему смыслу должны ждать готовности своих операндов.

В этом решении привлекает то, что все проблемы, связанные с назначением конкретных компьютеров, полностью игнорируются. Программа занимает процессоры по мере необходимости. Это происходит в не приведенных здесь командах создания, появляющихся, в частности, в процедуре вставки. Для вставки нового элемента в бинарное дерево создается вершина вызовом create new_node.make (new_element). Поскольку new_node имеет сепаратный тип BINARY_TREE1[G], для нее выделяется процессор. Связывание этих виртуальных процессоров с доступными физическими ресурсами происходит автоматически.

Замки

Предположим, что мы хотим разрешить многим клиентам, которых будем называть ключниками (lockers), получать исключительный доступ к сейфам - закрываемым ресурсам (lockable) - без явного выделения разделов, где происходит этот доступ, исключающий других ключников. Это даст нам механизм типа семафоров. Вот решение:

class LOCKER feature
    grab (resource: separate LOCKABLE) is        
            -- Запрос исключительного доступа к ресурсу
        require
            not resource.locked
        do
            resource.set_holder (Current)
        end
    release (resource: separate LOCKABLE) is
        require
            resource.is_held (Current)
        do
            resource.release
        end
end
class LOCKABLE feature {LOCKER}
    set_holder (l: separate LOCKER) is
            -- Назначает l владельцем
        require
            l /= Void
        do
            holder := l
        ensure
            locked
        end
    locked: BOOLEAN is
            -- Занят ли ресурс каким-либо ключником?
        do
            Result := (holder /= Void)
        end
    is_held (l: separate LOCKER): BOOLEAN is
            -- Занят ли ресурс l?
        do
            Result := (holder = l)
        end
    release is
            -- Освобождение от текущего владельца
        do
            holder := Void
        ensure
            not locked
        end
feature {NONE}
    holder: separate LOCKER
invariant
    locked_iff_holder: locked = (holder /= Void)
end

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

deferred class LOCKING_PROCESS feature
    resource: separate LOCKABLE
    use is
            -- Обеспечивает дисциплинированное использование resource
        require
            resource /= Void
        do
            from create lock; setup until over loop
                lock.grab (resource)
                exclusive_actions
                lock.release (resource)
            end
            finalize
        end
    set_resource (r: separate LOCKABLE) is
            -- Выбирает r в качестве используемого ресурса
        require
            r /= Void
        do
            resource := r
        ensure
            resource /= Void
        end
feature {NONE}
    lock: LOCKER
    exclusive_actions
            -- Операции во время исключительного доступа к resource
        deferred
        end
    setup
            -- Начальное действие; по умолчанию: ничего не делать
        do
        end
    over: BOOLEAN is
            -- Закончилось ли закрывающее поведение?
        deferred
        end
    finalize
            -- Заключительное действие; по умолчанию: ничего не делать
        do
        end
end

В эффективных наследниках класса LOCKING_PROCESS процедуры exclusive_actions и over будут эффективизированы, а setup и finalize могут быть доопределены. Отметим, что желательно писать класс LOCKING_PROCESS как наследник класса PROCESS.

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

Подпрограмма grab из класса LOCKER является примером того, что называется схемой визитной карточки: ресурсу resource передается ссылка на текущего ключника Current, трактуемая как сепаратная ссылка.

Основываясь на представляемых этими классами образцах, нетрудно написать и другие реализации разных видов семафоров (см. У12.7). ОО-механизмы помогают пользователям таких классов избежать классической опасности семафоров: выполнить для некоторого ресурса операцию резервирования reserve и забыть выполнить соответствующую операцию освобождения free. Разработчик, использующий класс поведения типа LOCKING_PROCESS, допишет отложенные операции в соответствии с нуждами своего приложения и сможет рассчитывать на то, что предопределенная общая схема обеспечит выполнение после каждой reserve соответствующей операции free.

Сопрограммы (Coroutines)

Хотя наш следующий пример и не является полностью параллельным (по крайней мере в его первоначальном виде), но он важен как способ проверки применимости нашего параллельного механизма.

Первым (и, возможно, единственным) из главных языков программирования, включившим конструкцию сопрограмм, был также и первый ОО-язык Simula 67; мы будем рассматривать его механизм сопрограмм при его описании в "От Simula к Java и далее: основные ОО-языки и окружения" . Там же будут приведены примеры практического использования сопрограмм.

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

  • При вызове обычной подпрограммы имеется хозяин и раб. Хозяин запускает подпрограмму, ожидает ее завершения и продолжает с того места, где закончился вызов; однако подпрограмма при вызове всегда начинает работу с самого начала. Хозяин вызывает, а подпрограмма-раб возвращает.
  • Отношения между сопрограммами - это отношения равных. Когда сопрограмма a застревает в процессе своей работы, то она призывает сопрограмму b на помощь; b запускается с того места, где последний раз остановилась, и продолжает выполнение до тех пор, пока сама не застрянет или не выполнит все, что от нее в данный момент требуется; затем a возобновляет свое вычисление. Вместо разных механизмов вызова и возврата здесь имеется одна операция возобновления вычисления resume c, означающая: запусти сопрограмму c с того места, в котором она последний раз была прервана, а я буду ждать, пока кто-нибудь не возобновит ( resumes ) мою работу.
Последовательность выполнения сопрограмм

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

Все это происходит строго последовательно и предназначено для выполнения в одном процессе (задании) одного компьютера. Но сама идея получена из параллельных вычислений; например, операционная система, выполняемая на одном ЦПУ, будет внутри себя использовать механизм сопрограмм для реализации разделения времени, многозадачности и многопоточности.

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

separate class COROUTINE creation
    make
feature {COROUTINE}
    resume (i: INTEGER) is
            -- Разбудить сопрограмму с идентификатором i и пойти спать
        do
            actual_resume (i, controller)
        end
feature {NONE} -- Implementation
    controller: COROUTINE_CONTROLLER
    identifier: INTEGER
    actual_resume (i: INTEGER; c: COROUTINE_CONTROLLER) is
            -- Разбудить сопрограмму с идентификатором i и пойти спать.
            -- (Реальная работа resume).
        do
            c.set_next (i); request (c)
        end
    request (c: COROUTINE_CONTROLLER) is
            -- Запрос возможного повторного пробуждения от c
        require
            c.is_next (identifier)
        do
            -- Действия не нужны
        end
feature {NONE} -- Создание
    make (i: INTEGER; c: COROUTINE_CONTROLLER) is
            -- Присвоение i идентификатору и c котроллеру
        do
            identifier := i
            controller := c
        end
end
separate class COROUTINE_CONTROLLER feature {NONE}
    next: INTEGER
feature {COROUTINE}
    set_next (i: INTEGER) is
        -- Выбор i в качестве следующей пробуждаемой сопрограммы
        do
            next := i
        end
    is_next (i: INTEGER): BOOLEAN is
        -- Является ли i индексом следующей пробуждаемой сопрограммы?
        do
            Result := (next = i)
        end
end

Одна или несколько сопрограмм будут разделять один контроллер сопрограмм, создаваемый не приведенной здесь однократной функцей (см. У12.10). У каждой сопрограммы имеется целочисленный идентификатор. Чтобы возобновить сопрограмму с идентификатором i, процедура resume с помощью actual_resume установит атрибут next контроллера в i, а затем приостановится, ожидая выполнения предусловия next = j, в котором j - это идентификатор самой сопрограммы. Это и обеспечит требуемое поведение.

Хотя это выглядит как обычная параллельная программа, данное решение гарантирует (в случае, когда у всех сопрограмм разные идентификаторы), что в каждый момент сможет выполняться лишь одна сопрограмма, что делает ненужным назначение более одного физического ЦПУ. (Контроллер мог бы использовать собственное ЦПУ, но его действия настолько просты, что этого не следует делать.)

Обращение к целочисленным идентификаторам необходимо, поскольку передача resume аргумента типа COROUTINE, т. е. сепаратного типа, вызвала бы блокировку. На практике можно воспользоваться объявлениями unique, чтобы не задавать эти идентификаторы вручную. Такое использование целых чисел имеет еще одно интересное следствие: если мы допустим, чтобы две или более сопрограмм имели одинаковые идентификаторы, то при наличии одного ЦПУ получим механизм недетерминированности: вызов resume (i) позволит перезапустить любую сопрограмму с идентификатором i. Если же ЦПУ будет много, то вызов resume (i) позволит параллельно выполняться всем сопрограммам с идентификатором i.

Таким образом, у приведенной схемы двойной эффект: в случае одного ЦПУ она обеспечивает работу механизма сопрограмм, а в случае нескольких ЦПУ - механизма управления максимальным числом одновременно активных процессов определенного вида.

Система управления лифтом

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

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

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

Данная реализация примера с лифтом, приспособленная к показу управления на экранах нескольких компьютеров через Интернет (а не для реальных лифтов), была использована на нескольких конференциях для демонстрации ОО-механизмов параллельности и распределенности.

Класс MOTOR описывает мотор, связанный с одной кабиной лифта и интерфейс с механическим оборудованием:

separate class MOTOR feature {ELEVATOR}
    move (floor: INTEGER) is
            -- Переместиться на этаж floor и сообщить об этом
        do
            "Приказать физическому устройству переместится на floor"
            signal_stopped (cabin)
        end
    signal_stopped (e: ELEVATOR) is
            -- Сообщить, что лифт остановился на этаже e
        do
            e.record_stop (position)
        end
feature {NONE}
    cabin: ELEVATOR
    position: INTEGER is
            -- Текущий этаж
        do
        Result := "Текущий этаж, считанный с физических датчиков"
        end
end

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

Причиной для выделения лифта и его мотора как сепаратных объектов является желание уменьшить "зернистость" запираний: сразу после того, как лифт пошлет запрос move своему мотору, он станет готов, благодаря политике ожидания по необходимости, к приему запросов от кнопок внутри и вне кабины. Он будет рассинхронизирован со своим мотором до получения вызова процедуры record_stop через процедуру signal_stopped. Экземпляр класса ELEVATOR будет только на очень короткое время зарезервирован вызовами от объектов классов MOTOR или BUTTON.

separate class ELEVATOR creation
    make
feature {BUTTON}
    accept (floor: INTEGER) is
            -- Записать и обработать запрос на переход на floor
        do
            record (floor)
            if not moving then process_request end
        end
feature {MOTOR}
    record_stop (floor: INTEGER) is
            -- Записать информацию об остановке лифта на этаже floor
        do
            moving := false; position := floor; process_request
        end
feature {DISPATCHER}
    position: INTEGER
    moving: BOOLEAN
feature {NONE}
    puller: MOTOR
    pending: QUEUE [INTEGER]
            -- Очередь ожидающих запросов
            -- (каждый идентифицируется номером нужного этажа)
    record (floor: INTEGER) is
            -- Записать запрос на переход на этаж floor
        do
            "Алгоритм вставки запроса на floor в очередь pending"
        end
    process_request is
            -- Обработка очередного запроса из pending, если такой есть
        local
            floor: INTEGER
        do
            if not pending.empty then
                floor := pending.item
                actual_process (puller, floor)
                pending.remove
            end
        end
    actual_process (m: separate MOTOR; floor: INTEGER) is
            -- Приказать m переместится на этаж floor
        do
            moving := True; m.move (floor)
        end
end

Имеются кнопки двух видов: кнопки на этажах, нажимаемые для вызова лифта на данный этаж, и кнопки внутри кабины, нажимаемые для перемещения лифта на соответствующий этаж. Эти два вида кнопок посылают разные запросы: запросы кнопок, расположенных внутри кабины, направляются этой кабине, а запросы кнопок на этажах могут обрабатываться любым лифтом, и поэтому они посылаются объекту-диспетчеру, опрашивающему разные лифты для выбора того, кто будет выполнять этот запрос. (Мы не приводим реализацию этого алгоритма выбора, поскольку это не существенно для данного рассмотрения, то же относится и к алгоритмам, используемым лифтами для управления их очередями запросов pending в классе ELEVATOR ).

В классе FLOOR_BUTTON предполагается, что на каждом этаже имеется только одна кнопка. Нетрудно изменить этот проект так, чтобы поддерживались две кнопки: одна для запросов на движение вверх, а другая - вниз.

Удобно, хотя и не очень существенно, иметь общего родителя BUTTON для классов, представляющих оба вида кнопок. Напомним, что компоненты, экспортируемые ELEVATOR в BUTTON, также экспортируются в соответствии со стандартными правилами скрытия информации в оба потомка этого класса:

separate class BUTTON feature
    target: INTEGER
end
separate class CABIN_BUTTON inherit BUTTON feature
    cabin: ELEVATOR
    request is
            -- Послать своему лифту запрос на остановку на этаже target
        do
            actual_request (cabin)
        end
    actual_request (e: ELEVATOR) is
            -- Захватить e и послать запрос на остановку на этаже target
        do
            e.accept (target)
        end
end
separate class FLOOR_BUTTON inherit
    BUTTON
feature
    controller: DISPATCHER
    request is
            -- Послать диспетчеру запрос на остановку на этаже target
        do
            actual_request (controller)
        end
    actual_request (d: DISPATCHER) is
            -- Послать d запрос на остановку на этаже target
        do
            d.accept (target)
        end
end

Вопрос о включении и выключении света в кнопках здесь не рассматривается. Нетрудно добавить вызовы подпрограмм, которые будут этим заниматься.

Наконец, вот класс DISPATCHER. Чтобы разработать алгоритм выбора лифта в процедуре accept, потребуется разрешить ей доступ к атрибутам position и moving класса ELEVATOR, которые в полной системе должны быть дополнены булевским атрибутом going_up (движется_вверх). Такой доступ не вызовет никаких проблем, поскольку наш проект гарантирует, что объекты класса ELEVATOR никогда не резервируются на долгое время.

separate class DISPATCHER creation
    make
feature {FLOOR_BUTTON}
    accept (floor: INTEGER) is
            -- Обработка запроса о посылке лифта на этаж floor
        local
            index: INTEGER; chosen: ELEVATOR
        do
"Алгоритм определения лифта, выполняющего запрос для этажа floor"
            index := "Индекс выбранного лифта"
            chosen := elevators @ index
            send_request (chosen, floor)
        end
feature {NONE}
    send_request (e: ELEVATOR; floor: INTEGER) is
            -- Послать лифту e запрос на перемещение на этаж floor
        do
            e.accept (floor)
        end
    elevators: ARRAY [ELEVATOR]
feature {NONE} -- Создание
    make is
            -- Настройка массива лифтов
        do
            "Инициализировать массив лифтов"
        end
end

Сторожевой механизм

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

Мы хотим дать возможность некоторому объекту вызвать некоторую процедуру action при условии, что этот вызов будет прерван и булевскому атрибуту failed будет присвоено значение истина ( true ), если процедура не завершит свое выполнение через t секунд. Единственным доступным средством измерения времени является процедура wait (t), которая будет выполняться в течение t секунд.

Приведем решение, использующее дуэль. Класс, которому нужен указанный механизм, будет наследником класса поведения TIMED и предоставит эффективную версию процедуры action, отложенной в классе TIMED. Чтобы разрешить action выполняться не более t секунд, достаточно вызвать timed_action (t). Эта процедура запускает сторожа (экземпляр класса WATCHDOG ), который выполняет wait (t), а затем прерывает клиента. Если же сама процедура action завершится в предписанное время, то сам клиент прервет сторожа. Отметим, что в приведенном классе у всех процедур с аргументом t: REAL имеется предусловие t>=0, опущенное для краткости.

deferred class TIMED inherit
    CONCURRENCY
feature {NONE}
    failed: BOOLEAN; alarm: WATCHDOG
    timed_action (t: REAL) is
-- Выполняет действие, но прерывается после t секунд, если не завершится
-- Если прерывается до завершения, то устанавливает failed в true
        do
            set_alarm (t); unset_alarm (t); failed := False
        rescue
            if is_concurrency_interrupt then failed := True end
        end
    set_alarm (t: REAL) is
 -- Выдает сигнал тревоги для прерывания текущего объекта через t секунд
        do
            -- При необходимости создать сигнал тревоги:
            if alarm = Void then create alarm end
            yield; actual_set (alarm, t); retain
        end
    unset_alarm (t: REAL) is
            -- Удалить последний сигнал тревоги
        do
            demand; actual_unset (alarm); wait_turn
        end
    action is
            -- Действие, выполняемое под управлением сторожа
        deferred
        end
feature {NONE} -- Реальный доступ к сторожу
    actual_set (a: WATCHDOG; t: REAL) is
            -- Запуск a для прерывания текущего объекта после t секунд
        do
            a.set (t)
        end
    ...Аналогичная процедура actual_unset предоставляется читателю...
feature {WATCHDOG} -- Операция прерывания
    stop is
-- Пустое действие, чтобы позволить сторожу прервать вызов timed_action
        do -- Nothing end
end
separate class
    WATCHDOG
feature {TIMED}
set (caller: separate TIMED; t: REAL) is
            -- После t секунд прерывает caller;
            -- если до этого прерывается, то спокойно завершается
        require
            caller_exists: caller /= Void
        local
            interrupted: BOOLEAN
        do
        if not interrupted then wait (t); demand; callerl stop; wait_turn end
        rescue
            if is_concurrency_interrupt then interrupted:= True; retry end
        end
    unset is
            -- Удаляет сигнал тревоги (пустое действие, чтобы дать
            -- клиенту прервать set)
        do -- Nothing end
feature {NONE}
    early_termination: BOOLEAN
end

За каждым использованием yield должно, как это здесь сделано, следовать retain в виде: yield ; "Некоторый вызов"; retain. Аналогично каждое использование demand (или insist ) должно иметь вид: demand ; "Некоторый вызов "; wait_turn. Для принудительного выполнения этого правила можно использовать классы поведения.

Организация доступа к буферам

Завершим рассмотрение примером ограниченного буфера, уже встречавшегося при описании механизма параллельности. Этот класс можно объявить как separate class BOUNDED_BUFFER [G] inherit BOUNDED_QUEUE [G] end в предположении, что имеется соответствующий последовательный класс BOUNDED_QUEUE.

Чтобы для сущности q типа BOUNDED_BUFFER [T] выполнить вызов вида q.remove, его нужно включить в подпрограмму, использующую q как формальный аргумент. Для этой цели полезно было бы разработать класс BUFFER_ACCESS ( ДОСТУП_К_БУФЕРУ ), инкапсулирующий понятие ограниченного буфера, а классы приложений могли бы стать его наследниками. В написании такого класса поведения нет ничего трудного. Он дает хороший пример того, как можно инкапсулировать сепаратные классы (непосредственно выведенные из последовательных, таких как BOUNDED_ QUEUE ) для облегчения их непосредственного использования в параллельных приложениях.

indexing
    description: "Инкапсуляция доступа к ограниченным буферам"
class BUFFER_ACCESS [G] is
    put (q: BOUNDED_BUFFER [G]; x: G) is
    -- Вставляет x в q, ожидая, при необходимости свободного места
        require
            not q.full
        do
            q.put (x)
        ensure
            not q.empty
        end
    remove (q: BOUNDED_BUFFER [G]) is
    -- Удаляет элемент из q, ожидая, при необходимости его появления
        require
            not q.empty
        do
            q.remove
        ensure
            not q.full
        end
    item (q: BOUNDED_BUFFER [G]): G is
            -- Старейший неиспользованный элемент
        require
            not q.empty
        do
            Result := q.item
        ensure
            not q.full
        end
end