154
отказываться от второй транзакции, обычно стоит подождать снятия блокировки с объекта текущей
транзакцией.
При отсутствии очередей вам не придется беспокоиться о ситуациях взаимной блокировки (когда
A
ожидает
B
, а
B
ожидает
A
). Если транзакция запросила блокировку и не смогла ее получить, она
уничтожается. Если очереди поддерживаются, код блокировки должен определить,
принадлежит ли
этой транзакции какие-либо блокировки, которых дожидаются остальные, и если принадлежат,
избавить одну из транзакций от бескенечного ожидания.
Происходящее оказывается в опасной близости от точки, в которой вам приходится либо разбивать
свое приложение на несколько подзадач и поручать операционной системе их планирование, либо
воспользоваться одной из коммерческих библиотек с поддержкой многопоточности. Так или иначе, в
этой области мы не узнаем ничего принципиально нового, относящегося к С++, и
поэтому не будем
развивать эту тему. Об очередях, блокировках и многопоточности написано немало хороших книг, вы
наверняка найдете их в разделе «Базы данных» своего книжного магазина.
Многоуровневая отмена
Семантика транзакций довольно легко распространяется и на многоуровневую отмену (если вспомнить
концепции
StackPtr
, о которых говорилось выше). Существует два основных варианта реализации.
Класс LockPtr со стеками образов
Самый прямолинейный вариант реализации многоуровневой отмены для транзакций — включение
стека старых образов в каждый
LockPtr
. Эта идея позаимствована из рассмотренного выше кода
StackPtr
. Тем не менее, она подходит лишь для консервативной блокировки. В случае с агрессивной
блокировкой объект может быть впервые заблокирован уже после изменения остальных объектов. Это
усложняет отмену нескольких изменений, поскольку стеки разных
LockPtr
не синхронизируются.
Стеки пар LockPtr/образ
К проблеме можно подойти и иначе — включить в
Transaction
стек, в котором хранятся пары старый
образ/
LockPtr
. На каждом уровне стека хранятся лишь те
LockPtr
, которые существовали на момент
закрепления. В общем случае это решение работает лучше, к тому же оно чуть более эффективно — вы
используете один большой стек вместо множества маленьких.
Оптимизация объема
Другое решение — реорганизация структур данных для сокращения издержек, связанных с хранением
незаблокированных
ConstPtr
(хотя и ценой некоторой потери скорости).
ConstPtr
лишь
незначительно отличается от указателей только для чтения, которые рассматривались в главе 6 и не
имели ничего общего с транзакциями: он имеет ссылку на
ConstPtr
и функцию
Lock()
. Мы
избавимся от первого и внесем некоторые изменения во второе.
Представьте себе глобальную структуру данных (вероятно, хранящуюся в виде статического члена
класса
Transaction
), в которой находится информация о том, какие
Transaction
блокируют какие
ConstPtr
. Для каждой пары в таблице содержится соответствующий
LockPtr
.
Каждый раз, когда
вызывается функция
Lock()
класса
ConstPtr
, она проверяет, присутствует ли
this
в таблице. Если
присутствует, функция сравнивает транзакцию, переданную в качестве аргумента, с находящейся в
таблице. Если
ConstPtr
не находит себя в таблице, он включает в нее новый триплет (
ConstPtr
,
Transaction
,
LockPtr
), а если находит с другой транзакцией — инициирует исключение.
Такая схема оказывается более экономной, чем описанная выше; она не
тратит память на значения
NULL
для всех незаблокированных объектов. Разумеется, она сложенее и медленнее работает —
структура данных еще только разогревает двигатель на старте, а косвенной обращение через
переменную класса уже пересекает финишную черту.
Возможно, у вас возник вопрос — а почему функция
Lock()
должна оставаться с
ConstPtr
? Почему
ее нельзя включить в другой класс или даже сделать глобальной функцией? Если мы избавимся от
переменной
LockPtr*
и функции
Lock()
,
ConstPtr
превратится в самый обычный указатель только
для чтения, который на вопрос о транзакциях лишь тупо смотрит на вопрошающего. Впрочем, так ли
это?
LockPtr
по-прежнему приходится объявлять другом; следовательно, хотя бы тривиальных