| Украина, Киев |
Параллельность, распределенность, клиент-сервер и Интернет
Примеры
Для иллюстрации предложенного механизма мы сейчас приведем несколько примеров, выбранных из различных источников - от традиционных примеров параллельных программ до приложений реального времени.
Обедающие философы
Знаменитые "обедающие философы" Дейкстры - искусственный пример, призванный проиллюстрировать поведение процессов операционной системы, конкурирующих за разделяемые ресурсы, является обязательной частью всякого обсуждения параллелизма. Пять философов, сидящих за столом, проводят время в размышлениях, затем едят, затем снова размышляют и т. д. Чтобы есть спагетти, каждому из них нужны две вилки, лежащие непосредственно слева и справа от философов, что создает предпосылку для возникновения блокировок.
Следующий класс описывает поведение философов. Благодаря механизму резервирования объектов с помощью сепаратных аргументов в нем, по существу, отсутствует явный код синхронизации (в отличие от обычно предлагаемых в литературе решений):
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 ) мою работу.
Все это происходит строго последовательно и предназначено для выполнения в одном процессе (задании) одного компьютера. Но сама идея получена из параллельных вычислений; например, операционная система, выполняемая на одном ЦПУ, будет внутри себя использовать механизм сопрограмм для реализации разделения времени, многозадачности и многопоточности.
Можно рассматривать сопрограммы как некоторый ограниченный вид параллельности: бедный суррогат параллельного вычисления, которому доступна лишь одна ветвь управления. Обычно полезно проверять общие механизмы на их способность элегантно упрощаться для работы в ограниченных ситуациях, поэтому давайте посмотрим, как можно представить сопрограммы. Эта цель достигается с помощью следующих двух классов:
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

