Основы объектно-ориентированного проектирования


Резервирование объекта


Нам нужен способ, обеспечивающий некоторому клиенту исключительные права доступа к некоторому ресурсу, предоставляемому некоторым объектом.

Идея, привлекательная на первый взгляд (но недостаточная), основана на использовании понятия сепаратного вызова. Рассмотрим вызов x.f (...) для сепаратной сущности x, присоединенной во время выполнения к объекту O2, где вызов выполняется некоторым объектом-клиентом O1. Ясно, что после начала выполнения этого вызова O1 может безопасно перейти к своему следующему делу, не дожидаясь его завершения, но само выполнение этого вызова не может начаться до тех пор, пока O2 не освободится для O1. Отсюда можно заключить, что клиент дождется, когда целевой объект станет свободным, и клиент сможет выполнять над ним свою операцию.

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

Buffer.remove; buffer.remove,

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

Одно решение состоит в добавлении к порождающему классу buffer (или его потомку) процедуры remove_two, удаляющей одновременно два элемента. Но в общем случае это решение нереалистично: нельзя изменять поставщика из-за каждой потребности в синхронизации кода их клиентов. У клиента должна быть возможность удерживать поставленный ему объект так долго, как это требуется.

Другими словами, нужно нечто в духе механизма критических интервалов. Ранее был введен их синтаксис:

hold a then действия_требующие_исключительного_доступа end

Или в условном варианте:

hold a when a.некоторое_свойство then действия_требующие_исключительного_доступа end

Тем не менее, мы перейдем к более простому способу обозначений, возможно, вначале несколько странному.
Наше соглашение состоит в том, что, если a - это непустое сепаратное выражение, то вызов вида:

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

Заметим, что ожидание имеет смысл, только если подпрограмма содержит хоть один вызов x.some_routine с формальным аргументом x, соответствующим a. В противном случае, например, если она выполняет только присваивание вида some_attribute := x, ждать нет никакой необходимости. Это будет уточнено в полной форме правила, которое будет сформулировано далее в этой лекции.
Возможны и другие подходы, в которых авторы предлагают сохранить инструкцию hold. Но передача аргумента в качестве механизма резервирования объекта сохраняет простоту и легкость освоения модели параллелизма. Схема с hold привлекательна для разработчиков, поскольку соответствует девизу ОО-разработки "Инкапсулировать повторения", а главное, объединяет в одной подпрограмме действия, требующие исключающего доступа к объекту. Поскольку этой подпрограмме неизбежно потребуется аргумент, представляющий сам объект, то мы пошли дальше и считаем наличие такого аргумента достаточным для обеспечения резервирования объекта без введения ключевого слова hold.

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

r (a: separate SOME_TYPE) is do ...; a.r1 (...); ... ...; a.r2 (...); ... endреализация может продолжать выполнение остальных инструкций, не дожидаясь завершения любого из двух вызовов при условии, что она протоколирует вызовы для a так, что они будут выполняться в требуемом порядке. (Нам еще нужно понять, как, если это потребуется, ждать завершения сепаратного вызова; пока же, мы просто запускаем вызовы и никогда не ждем!)

Если у подпрограммы имеется несколько сепаратных аргументов, то вызов клиента будет ждать, пока не сможет зарезервировать все соответствующие объекты. Это требование тяжело реализовать при компиляции, которая должна порождать код, используя протоколы для множественного одновременного резервирования. По этой причине компилятор мог бы требовать, чтобы подпрограмма использовала не более одного сепаратного формального аргумента. Но при полной реализации предложенного механизма разработчики приложений получают значительные удобства; в частности, рассматриваемая в качестве типичного примера далее в этой лекции знаменитая проблема "обедающих философов" допускает почти тривиальное решение.


Содержание раздела