Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции - Часть 10.doc
Скачиваний:
36
Добавлен:
02.05.2014
Размер:
3.62 Mб
Скачать

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;

Класс Хранилище Показаний Аналоговых Датчиков может пользоваться все­ми преимуществами нового решения, хотя мы не изменили в нем ни одной строчки.