Рис. 9-11. Механизм блокировки.

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

Описания всех функций-членов синхронизированного класса используют блокировки для 'оборачивания' операций, унаследованных из суперкласса. Рассмотрим в качестве примера реализацию функции length для синхронизированной неограниченной очереди:

template<class Item, class StorageManager, class Monitor> unsigned int SynchronizedUnboundedQueue<Item, StorageManager, Monitor>::length() const {

ReadLock lock(monitor); return UnboundedQueue<Item, StorageManager>::length();

}

Данный фрагмент кода иллюстрирует механизм, приведенный на рис. 9-11. Как правило, объекты класса ReadLock используются для всех синхронизированных селекторов, а экземпляры WriteLock - для синхронизированных модификаторов. Простота и элегантность подобной архитектуры проявляется в том, что каждая функция представляет собой законченную операцию, в любом случае гарантирующую сохранность состояния объекта, причем без каких-либо явных действий со стороны агентов чтения/записи.

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

Наша архитектура обеспечивает синхронизированным формам отсутствие ситуаций типа 'смертельное объятие'. Например, операции присваивания объекта самому себе или сравнения его с самим собой потенциально опасны, так как требуют блокировки и левого и правого элементов выражения, которые в данном случае являются одним и тем же объектом. Будучи создан, объект не может изменить свою идентичность, поэтому тесты на самоидентичность выполнятся до блокировки какого-либо объекта. Именно поэтому описанный ранее оператор присваивания operator= включал такую проверку, как показывает следующая сокращенная запись:

template<class Item> Queue<Item>& Queue<Item>::operator=(const Queue<Item>& q) {

if (this == &q) return *this;

}

Любые функции-члены, среди аргументов которых есть экземпляры класса, к которому они принадлежат, должны проектироваться так, чтобы обеспечивалась корректная схема блокировки этих аргументов. Наше решение базируется на полиморфизме двух служебных функций, lock и unlock, определенных в каждом абстрактном базовом классе. Каждый абстрактный базовый класс по умолчанию содержит заглушку для этих двух функций; синхронизированные формы обеспечивают захват и освобождение аргумента. Вот почему описанный ранее оператор присваивания operator= включал вызовы этих двух функций, как показывает следующая сокращенная запись:

template<class Item> Queue<Item>& Queue<Item>::operator=(const Queue<Item>& q) {

((Queue<Item>&)q).lock(); ((Queue<Item>&)q).unlock (); return *this;

}

Явное приведение типа используется в данном случае для того, чтобы освободиться от ограничения const на аргумент.

9.3. Эволюция

Проектирование интерфейса классов

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

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

setHashFunction   Устанавливает функцию хеширования для элементов множества. 

 • clear   Очищает множество. 

 • add   Добавляет элемент к множеству. 

 • remove   Удаляет элемент из множества. 

 • setUnion   Объединяет с другим множеством. 

 • intersection   Находит пересечение с другим множеством. 

 • difference   Удаляет элементы, которые содержатся в другом множестве. 

 • extent   Возвращает количество элементов в множестве. 

 • isEmpty   Возвращает 1, если множество пусто. 

 • isMember   Возвращает 1, если данный элемент принадлежит множеству. 

 • isSubset   Возвращает 1, если множество является подмножеством другого множества. 

 • isProperSubset   Возвращает 1, если множество является собственным подмножеством другого множества. 

  Подобным же образом можно определить протокол класса BinaryTree:  

clear   Уничтожает дерево и всех его потомков. 

 • insert   Добавляет новый узел в корень дерева. 

 • append   Добавляет к дереву потомка. 

 • remove   Удаляет потомка из дерева. 

 • share   Структурно делит данное дерево. 

 • swapChild   Переставляет потомка с деревом. 

 • child   Возвращает данного потомка. 

 • leftChild   Возвращает левого потомка. 

 • rightChild   Возвращает правого потомка. 

 • parent   Возвращает родителя дерева. 

 • setItem   Устанавливает элемент, ассоциированный с деревом. 

 • hasChildren   Возвращает 1, если у дерева есть потомки. 

 • isNull   Возвращает 1, если дерево нулевое. 

 • isShared   Возвращает 1, если дерево структурно разделено. 

 • isRoot   Возвращает 1, если дерево имеет корень. 

Добавить отзыв
ВСЕ ОТЗЫВЫ О КНИГЕ В ИЗБРАННОЕ

0

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

Отметить Добавить цитату