10.2. Синхронизация доступа к классам
Если к классу может обращаться несколько задач, то его операции должны обеспечивать синхронизацию доступа к инкапсулируемым данным. Ниже описываются соответствующие механизмы: алгоритм взаимного исключения и алгоритм нескольких читателей и писателей.
10.2.1. Пример синхронизации доступа к классу. В качестве примера рассмотрим класс абстрагирования данных Хранилище Показаний Аналоговых Датчиков. При проектировании этого класса нужно принять решение о том, будет ли информация храниться в массиве или в связанном списке. Другое решение касается синхронизации: может ли к объекту такого класса осуществляться параллельный доступ и, если да, следует использовать алгоритм взаимного исключения или алгоритм читателей и писателей. Оба решения относятся только к проектированию класса, не затрагивая его пользователей.
Отделив вопрос о том, что делает класс (спецификация операций), от того, как это делается (проект класса), мы можем изолировать пользователей от модификаций внутреннего устройства класса. Допустимо следующее:
– трансформировать внутренние структуры данных, например применить связанный список вместо массива;
– заменить внутренний механизм синхронизации доступа к данным, в частности использовать алгоритм читателей и писателей вместо взаимного исключения.
Описанные изменения влияют только на внутреннее устройство класса: на внутренние структуры данных и внутренние операции, которые имеют доступ к этим структурам.
10.2.2. Операции класса абстрагирования данных. Сохранив внешний интерфейс класса Хранилище Показаний Аналоговых Датчиков, рассмотрим два разных метода синхронизации доступа к хранилищу:взаимное исключение и алгоритм читателей и писателей.
Наш класс предоставляет две операции (рис.10.3):
читатьАналоговыйДатчик (in идДатчика, out значениеДатчика,
out верхнийПредел, out нижнийПредел, out условиеТревоги)
Эта операция вызывается задачами-читателями, которые хотят получить показания датчиков из хранилища. Она возвращает значение датчика с заданным идентификатором, его верхний и нижний пределы, а также условие тревоги. Если значение попадает в диапазон между верхним и нижним пределом, то датчик функционирует нормально. Если же значение оказывается меньше нижнего или больше верхнего предела, то условие тревоги будет равно нижнему или верхнему пределу соответственно.
обновитьАналоговыйДатчик (in идДатчика, in значениеДатчика)
Данная операция вызывается задачами-писателями, которые хотят поместить в хранилище новое значение датчика, прочитанное из внешней среды. Операция проверяет, попало ли значение в безопасный диапазон, и, если это не так, устанавливает соответствующее значение условияТревоги.
Рис.10.3. Пример параллельного доступа к объекту абстрагирования данных
10.2.3. Синхронизация методом взаимного исключения. Сначала рассмотрим решение, основанное на взаимном исключении. В таком случае используется предоставляемый операционной системой двоичный семафор, у которого есть операции acquire (запросить) и release (освободить). Чтобы гарантировать взаимное исключение, каждая задача должна вызвать операцию acquire семафора readWriteSemaphore (изначально он установлен в единицу) перед тем, как пытаться получить доступ к хранилищу. Закончив работу с хранилищем, задача обращается к операции release. Вот псевдокод операций чтения и обновления:
class ХранилищеПоказанийАналоговыхДатчиков
private readWriteSemaphore : Semaphore := 1
public читатьАналоговыйДатчик (in идДатчика;,
out значениеДатчика, out верхнийПредел,
out нижнийПредел, out условиеТревоги)
-- Критическая секция операции чтения.
acquire (readWriteSemaphore) ;
значениеДатчика := хранилищеПоказаний
(идДатчика, значение);
верхнийПредел := хранилищеПоказаний
(идДатчика, верх);
нижнийПредел := хранилищеПоказаний
(идДатчика, низ);
условиеТревоги := хранилищеПоказаний
(идДатчика; тревога);
release(readWriteSemaphore);
end читатьАналоговыйДатчик;
При выполнении операции обновить надо не только записать в хранилище новое значение датчика, но и проверить условие тревоги:
public обновитьАналоговыйДатчик (in идДатчика,
in значениеДатчика)
-- Критическая секция операции записи.
acquire (readWriteSemaphore);
хранилищеПоказаний (идДатчика, значение) :=
значениеДатчика;
if значениеДатчика >= хранилищеПоказаний
АналоговыхДатчиков(идДатчика, верх)
then хранилищеПоказаний (идДатчика,
тревога) := верх;
elseif значениеДатчика<= хранилищеПоказа-
ний(идДатчика, низ)
then хранилищеПоказаний (идДатчика,
тревога) := низ;
else хранилищеПоказаний (идДатчика,
тревога) := норма;
end if;
release(readWriteSemaphore);
end обновитьАналоговыйДатчик;
10.2.4. Синхронизация нескольких читателей и писателей. Применение указанного метода позволяет нескольким читателям одновременно обращаться к хранилищу, но любой писатель получает монопольный доступ. Для этого применяются два двоичных семафора: readerSemaphore и readWriteSemaphore, инициализированных значением 1. Кроме того, хранится текущее число Читателей, первоначально равное нулю. Семафор readerSemaphore используется читателями, чтобы гарантировать взаимно исключающее обновление счетчика читателей. Семафор readWriteSemaphore задействован писателями для обеспечения взаимно исключающего доступа к хранилищу. Но к данному семафору обращаются также и читатели. Он захватывается первым читателем перед началом чтения из хранилища и освобождается последним читателем, закончившим чтение. Ниже приведен псевдокод операция чтения и обновления:
class ХранилищеПоказанийАналоговыхДатчиков
private числоЧитателей : Integer : = 0;
readerSemaphore :. Semaphore := 1;
readWriteSemaphore : Semaphore := 1;
public читатьАналоговыйДатчик (in идДатчика,
out значениеДатчика, out верхнийПредел,
out нижнийПредел, out условиеТревоги)
-- Операция чтения вызывается задачами-
-- читателями. Доступ к хранилищу разрешен
-- одновременно нескольким читателям
-- при условии, что нет ни одного писателя.
acquire (readerSemaphore) ;
Увеличить числоЧитателей;
if числоЧитателей = 1
then acquire (readWriteSemaphpre) ;
release (readerSemaphore) ;
значениеДатчика := хранилищеПоказаний
(идДатчика, значение);
верхнийПредел := хранилищеПоказаний
(идДатчика, верх);
нижнийПредел := хранилищеПоказаний
(идДатчика, низ);
условиеТревоги := хранилищеПоказаний
(идДатчика, тревога);
acquire (readerSemaphore) ;
Уменьшить числоЧитателей;
if числоЧитателей = 0
then release (readWriteSemaphore);
release(readerSemaphore);
end читатьАналоговыйДатчик;
Псевдокод операции обновления аналогичен написанному ранее для алгоритма взаимного исключения, поскольку писатели, желающие обновить хранилище, должны обеспечить взаимно исключающий доступ к нему:
public обновитьАналоговыйДатчик (in идДатчика,
in значениеДатчика)
-- Критическая секция операции записи.
acquire (readWriteSemaphore) ;
хранилищеПоказаний (идДатчика, значение) :=
значениеДатчика;
if значениеДатчика >= хранилищеПоказаний
(идДатчика, верх)
then хранилищеПоказаний (идДатчика,
тревога) := верх;
elseif значениеДатчика <=
хранилищеПоказаний (идДатчика,. низ)
then хранилищеПоказаний (идДатчика,
тревога) := низ;
else хранилищеПоказаний (идДатчика,
тревога) := норма;
end if;
release(readWriteSemaphore) ;
end обновитьАналоговыйДатчик;
Проблема решена, но код синхронизации оказался переплетенным с кодом доступа к хранилищу. Такие обязанности желательно развести, и в следующем разделе мы покажем, как это сделать.
10.2.5. Синхронизация нескольких читателей и писателей с помощью монитора. Ниже речь пойдет о применении мониторов для решения проблемы нескольких читателей и писателей. Напомним, что операции монитора выполняются в условиях взаимного исключения, поэтому представленное выше решение задачи о доступе к хранилищу показаний аналоговых датчиков методом взаимного исключения легко реализуется и с помощью мониторов. Однако к задаче посредством алгоритма читателей и писателей мониторы напрямую применяться не могут, так как операция читатьАналоговый Датчик должна одновременно выполняться несколькими читателями. Поэтому мы поступим иначе: инкапсулируем аспекты алгоритма читателей и писателей, касающиеся синхронизации, в монитор и перепроектируем класс Хранилище Показаний Аналоговых Датчиков. Приведем два решения. Первое реализует ту же функциональность, что и в предыдущем разделе, а второе обладает дополнительными функциями, предотвращающими ущемление писателей.
Объявим монитор ReadWrite, который использует два семафора и предоставляет четыре взаимно исключающие операции. Семафоры назовем reader Semaphore и readWriteSemaphore. Четыре вышеупомянутые операции – это startRead, endRead, startWrite и endWrite. Задача-читатель вызывает операцию startRead перед началом чтения и операцию endRead после его окончания. Задача-писатель вызывает операцию startWrite перед началом чтения и операцию endWrite после его окончания. Устройство монитора на базе семафоров описано в разделе 3.8.2. Такой монитор предоставляет для захвата ресурса операцию acquire, которая может приостановить задачу, если ресурс занят, а также операцию release для освобождения ресурса.
Операция startRead сначала должна захватить семафор readerSemaphore, увеличить число читателей и освободить семафор. Если счетчик читателей был равен нулю, то startRead должна захватить семафор readWriteSemaphore, который занимает первый читатель, а освобождает последний. Хотя операции монитора выполняются взаимно исключающим образом, семафор readerSemaphore все же нужен. Дело в том, что читатель может быть приостановлен в ожидании семафора readWriteSemaphore, в подобном случае он разблокирует монитор ReadWrite. Если другой читатель в этот момент захватит монитор, вызвав startRead или endRead, он будет ожидать семафора readerSemaphore. Ниже представлен проект монитора ReadWrite:
monitor ReadWrite
-- Предназначен для предоставления доступа к
-- ресурсам нескольким читателям и одному
-- писателю.
-- Объявляет целочисленный счетчик числа
-- читателей.
-- Объявляет семафор для доступа к счетчику
-- читателей.
-- Объявляет семафор для взаимно
-- исключающего доступа к буферу.
private числоЧитателей : Integer = 0;
readerSemaphore : Semaphore;
readWriteSemaphore : Semaphore;
public startRead ()
-- Читатель вызывает эту операцию перед
-- началом чтения.
readerSemaphore.acquire;
if числоЧитателей = 0
then readWriteSemaphore.acquire;
Увеличить числоЧитателей;
readerSemaphore.release;
end startRead;
public endRead ()
-- Читатель вызывает эту операцию после
-- окончания чтения.
readerSemaphore.acquire ;
Уменьшить числоЧитателей;
if числоЧитателей = 0
then readWriteSemaphore.release;
readerSemaphore.release;
end endRead;
public startWrite ()
--Писатель вызывает эту операцию перед
-- началом записи.
readWriteSemaphore.acquire;
end startWrite;
public endWrite ()
-- Писатель вызывает эту операцию после
-- окончания записи.
readWriteSemaphore.release;
end endWrite;
end ReadWrite;
Теперь перепроектируем класс Хранилище Показаний Аналоговых Датчиков так, чтобы он мог воспользоваться монитором ReadWrite. Для этого объявим в нем закрытый экземпляр монитора. Операция читатьАналоговыйДатчик теперь вызывает операцию монитора startRead перед началом чтения из хранилища и операцию endRead после его окончания. Операция обновитьАналоговыйДатчик вызывает операцию монитора startWrite перед началом записи в хранилище и операцию endWrite после ее окончания.
class ХранилищеПоказанийАналоговыхДатчиков
private multiReadSingleWrite : ReadWrite
public читатьАналоговыйДатчик (in идДатчика,
out значениеДатчика, out верхнийПредел,
out нижнийПредел, out условиеТревоги)
multiReadSingleWrite.startRead();
значениеДатчика : == хранилищеПоказаний
(идДатчика, значение);
верхнийПредел := хранилищеПоказаний
(идДатчика, верх);
нижнийПредел := хранилищеПоказаний
(идДатчика, низ);
условиеТревоги := хранилищеПоказаний
(идДатчика, тревога);
multiReadSingleWrite.endRead() ;
end читатьАналоговыйДатчик;
public обновитьАналоговыйДатчик (in идДатчика,
in значениеДатчика)
-- Критическая секция операции записи.
multiReadSingleWrite.startWrite() ;
хранилищеПоказаний (идДатчика, значение) :=
значениеДатчика;
if значениеДатчика >= хранилищеПоказаний
(идДатчика, верх)
then хранилищеПоказаний (идДатчика,
тревога) := верх;
elseif значениеДатчика <=
хранилищеПоказаний (идДатчика, низ)
then хранилищеПоказаний (идДатчика,
тревога) := низ;
else хранилищеПоказаний (идДатчика,
тревога) := норма;
end if;
multiReadSingleWrite.endWrite();
end обновитьАналоговыйДатчик;
end ХранилищеПоказанийАналоговыхДатчиков;
10.2.6. Синхронизация нескольких читателей и писателей без ущемления писателей. В предыдущем варианте решения есть одно неудобство. Если от читателей постоянно поступают запросы на чтение, писателю в течение неопределенно долгого времени может быть отказано в доступе к хранилищу. Этот феномен называется ущемлением писателя (writer starvation). Ниже показано решение проблемы путем введения дополнительного семафора writer Waiting. Операция startWrite теперь должна захватить семафор writerWaiting до захвата семафора readWrite Semaphore. Операция startRead захватывает (и освобождает) семафор writerWaiting перед тем, как захватить reader Semaphore.
Поясним, для чего нужны такие изменения. Предположим, что несколько читателей заняты чтением, а писатель в этот момент пытается обновить данные. Он успешно захватывает семафор writerWaiting, но дальше вынужден ждать, пока читатели освободят семафор readWriteSemaphore. Если приходит новый читатель, то он вызывает startRead и приостанавливается, ожидая освобождения семафора writerWaiting. Тем временем уже имеющиеся читатели заканчивают свои операции, и последний освобождает семафор readWriteSemaphore, который тут же переходит в распоряжение ожидающего писателя, а тот освобождает семафор writerWaiting, позволяя занять его читателю или писателю. В приведенном ниже псевдокоде реализации операций startRead и startWrite изменены:
monitor ReadWrite
-- Предотвращает ущемление писателей за счет
-- дополнительного семафора.
-- Предназначен для предоставления доступа к
-- ресурсам нескольким читателям и одному
-- писателю.
-- Объявляет целочисленный счетчик числа
-- читателей.
-- Объявляет семафор для доступа к счетчику
-- читателей.
-- Объявляет семафор для взаимно
-- исключающего доступа к буферу.
-- Объявляет семафор для ожидающих
-- писателей.
private числоЧитателей : Integer = 0;
readerSemaphore : Semaphore;
readWriteSemaphore: Semaphore;
writerWaitingSemaphore : Semaphore;
public startRead ()
-- Читатель вызывает эту операцию перед
-- началом чтения.
writerWaitingSemaphore.acquire;
writerWaitingSemaphore.release;
readerSemaphore.acquire;
if числоЧитателей = 0
then readWriteSemaphore.acquire;
Увеличить числоЧитателей;
readerSemaphore.release;
end startRead;
public endRead ()
-- Читатель вызывает эту операцию после
-- окончания чтения.
readerSemaphore.acqui.re;
Уменьшить числоЧитателей;
if числоЧитателей = 0
then readWriteSemaphore.release;
readerSemaphore.release;
end endRead;
public startWrite ()
-- Писатель вызывает эту операцию перед
-- началом записи.
writerWaitingSemaphore.acquire;
readWriteSemaphore.acquire;
writerWaitingSemaphore.release;
end startWrite;
public endWrite ()
-- Писатель вызывает эту операцию после
-- окончания записи.
readWriteSemaphore.release;
end endWrite;
end ReadWrite;
Класс Хранилище Показаний Аналоговых Датчиков может пользоваться всеми преимуществами нового решения, хотя мы не изменили в нем ни одной строчки.