Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Sb97573

.pdf
Скачиваний:
4
Добавлен:
13.02.2021
Размер:
685.79 Кб
Скачать

loop

D := Буфер.Прочитать; Обработка(D);

end loop;

Parbegin Поставщик; Потребитель; Parend.

По определению, в каждый конкретный момент времени процедура Записать или процедура Прочитать может использоваться только одной задачей; например, в случае, если «Поставщик» выполняет запись в буфер посредством вызова Буфер.Записать(D) и в это время «Потребитель» выполняет вызов Буфер.Прочитать, то «Потребитель» будет заблокирован и поставлен в очередь доступа. Таким образом, одновременное выполнение действий чтения и записи невозможно, т. е. выполняется третье условие задачи.

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

При определении монитора Хоаром был предложен механизм условной переменной, который является его неотъемлемой частью. В мониторе предоставляется возможность определять переменную типа condition, над которой возможны 3 операции: wait(cond), signal(cond), check(cond), где cond

переменная типа condition. Действие wait(cond) блокирует вызвавшую задачу и ставит ее в очередь, связанную с переменной cond; действие signal(cond) выбирает из этой очереди первую задачу (предполагается, что дисциплина обслуживания очереди FIFO) и позволяет ей продолжить работу; операция check(cond) возвращает количество задач, находящихся в этой очереди. Тип данных condition отличается от традиционного представления типа данных в языках программирования как определения множества значений переменной и множества допустимых операций над этими значениями.

Какие же очереди создаются при определении монитора? Во-первых – это очередь доступа к монитору. В нее попадают все задачи, которые блокируются при попытке получить доступ к ресурсам монитора, если он уже занят какой-то задачей. Далее, при определении в мониторе условной переменной с ней связывается очередь задач, которые могут блокироваться

11

действием wait над этой переменной. Таких очередей будет столько, сколько переменных определено в мониторе.

Приведем пример монитора, с помощью которого еще раз реализуем решение задачи «Поставщик – Потребитель»:

monitor Буфер;

СамБуфер array[1..ДлинаБуфера] : Данное; СчетчикЗаписей : integer;

НеПуст, НеПолон : condition; procedure Записать(d : Данное);

begin

if (СчетчикЗаписей >= ДлинаБуфера) then wait(НеПолон);

Записать_в_буфер; СчетчикЗаписей := СчетчикЗаписей + 1; signal(НеПуст);

end;

function Прочитать : Данное; begin

if (СчетчикЗаписей = 0) then wait(НеПуст);

Прочитать_из_буфера; СчетчикЗаписей := СчетчикЗаписей – 1; signal(НеПолон);

end;

begin

СчетчикЗаписей := 0; end Буфер;

Поставщик: loop

D := Производство;

Буфер.Записать(D); end loop;

Потребитель: loop

D := Буфер.Прочитать; Обработка(D);

12

end loop;

Parbegin Поставщик; Потребитель; Parend.

В приведенном мониторе выполняются все 3 условия задачи «Поставщик – Потребитель» в ее «классической интерпретации». Однако одна особенность заслуживает более тщательного рассмотрения.

Обратимся к примеру монитора, в котором описана одна условная condпеременная и две процедуры P1 и P2:

monitor Х;

cond : condition; procedure P1;

. . .

if (условие) then wait(cond);

. . .

end; procedure P2;

. . .

signal(cond);

. . .

операция Y;

. . .

end; end X.

Пусть некоторая задача R1 вызвала процедуру P1 при значении «условие» = «истина». Задача R1 блокируется и ставится в очередь, связанную с переменной cond, монитор освобождается, и его процедуры становятся доступными для других задач. Пусть теперь некоторая задача R2 устанавливает «условие» в значение «ложь» и вызывает процедуру P2. При этом действие операции signal(cond) должно немедленно активизировать задачу R1, гарантируя, что за время активизации значение условия «условие» не изменилось. Предполагается, что такая особенность, известная как «семантика Хоара», свойственна монитору Хоара.

Однако на практике реализация такого свойства оказывается сложной и не совместимой с вытесняющей многозадачностью, когда любая задача может быть приостановлена в произвольный момент. В реальной ситуации действие signal(cond) не прерывает задачу R2, а просто переводит R1 из

13

очереди заблокированных в очередь задач, готовых к выполнению, а задача R2 «держит» монитор до окончания выполнения процедуры P2. Такая реализация известна под названием «семантика Mesa». Заметим, что при этом нет никаких ограничений на то, что в приведенном примере действие «операция Y» снова не изменит значение «условие» на «истина». Кроме того за время, прошедшее между освобождением монитора задачей R2 и возобновлением работы R1 с действия, следующего за wait(cond), какая-то третья задача могла изменить значение «условие» на «истина». Таким образом, в «семантике Mesa», которая и реализуется на практике, может возникнуть ситуация, когда задача R1 будет продолжать работу при недопустимом значении условия. Во избежание этого условие блокировки задачи должно перепроверяться, и вместо конструкции «if (условие) then wait(cond);» необходимо использовать конструкцию «while (условие) wait(cond);».

Пример: задача «Читатели – Писатели»

В качестве примера использования монитора, предложенного Хоаром, рассмотрим еще одну задачу синхронизации, ставшую уже классической.

Пусть имеется некоторый «Информационный фонд», предназначенный для хранения определенных данных. Пусть имеется N задач – Писателей, которые могут записывать данные в этот Информационный фонд, и М задач

– Читателей, которые могут читать данные. Как Читатели, так и Писатели могут работать с фондом ограниченное время, но на скорость их работы ограничений не накладывается. Другими словами, какой-то Читатель может читать «очень быстро» и «недолго», а какой-то очень «медленно» и «долго». Аналогично и Писатели. Следует организовать работу Читателей и Писателей таким образом, чтобы выполнялись следующие условия:

1.В каждый момент времени может работать только один Писатель.

2.В каждый момент времени количество работающих Читателей может быть от 0 до М.

3.Ни Читатель, ни Писатель не должны ждать доступа к Фонду бесконечно долго.

Решить задачу можно с помощью монитора по следующей схеме: monitor ЧитателиПисатели;

МожноЧитать, МожноПисать : condition; КтоТоПишет : boolean;

14

Читатели : 0..M; procedure НачалоЧтения; procedure КонецЧтения; procedure НачалоЗаписи; procedure КонецЗаписи; begin

КтоТоПишет := false; Читатели := 0;

end ЧитателиПисатели.

Каждый Читатель начинает свое взаимодействие с Информационным фондом с вызова процедуры ЧитателиПисатели.НачалоЧтения и заканчивает вызовом ЧитателиПисатели.КонецЧтения:

Читатель: loop

ЧитателиПисатели.НачалоЧтения; РаботаСФондомЧ; ЧитателиПисатели.КонецЧтения; РазноеЧ;

end loop;

Аналогично, каждый Писатель выполняет подобные действия перед началом и концом записи:

Писатель: loop

ЧитателиПисатели.НачалоЗаписи; РаботаСФондомП; ЧитателиПисатели.КонецЗаписи; РазноеП;

end loop;

Таким образом, осталось определить 4 процедуры монитора, но сделать это так, чтобы выполнялись условия задачи. Начнем с процедуры НачалоЧтения:

procedure НачалоЧтения; begin

if (КтоТоПишет) or (check(МожноПисать) > 0) then

wait(МожноЧитать);

// *

Читатели := Читатели + 1;

 

15

signal(МожноЧитать);

// **

end;

 

Предполагается, что если к моменту вызова этой процедуры Читателем какой-либо Писатель уже работает с Информационным фондом, то он, Писатель, перед началом работы установил флаг КтоТоПишет = true, и, следовательно, Читатель не может начать работу. Другими словами, этим условием обеспечивается невозможность работы Читателей, когда работает Писатель. Остановимся теперь на условии check(МожноПисать) > 0 и покажем, зачем нужно было его использовать. Пока ничего не говорим о работе Писателей, однако предположим, что в какой-то момент времени несколько Читателей уже работают и некий Писатель пытается получить доступ к Информационному фонду. Очевидно, что этот Писатель будет заблокирован и поставлен в очередь, связанную с переменной МожноПисать. Таким образом, с помощью выражения check(МожноПисать) > 0 проверяется условие, есть ли Писатели, желающие работать, но заблокированные и не имеющие такой возможности, поскольку есть работающие Читатели. Но при этом новый Читатель активизироваться не сможет, так как значение check(МожноПисать) > 0 будет истинным и заблокированный Писатель получит доступ к Фонду, когда все уже работающие Читатели завершат свою деятельность (предполагается, что время работы с фондом как любого Читателя, так и любого писателя ограничено). Если отказаться от проверки этого условия, то можно предположить следующую ситуацию. Допустим, что среди работающих Читателей есть несколько очень активных, постоянно прекращающих и снова начинающих работу (ведь на скорость работы ни Читателей, ни Писателей ограничений не накладывается). Тогда существует ненулевая вероятность того, что Число работающих Читателей всегда будет больше нуля и заблокированные Писатели будут бесконечно долго ждать своей очереди.

Процедура КонецЧтения достаточно проста, в ней ведется подсчет работающих Читателей, и если их нет, то посылается сигнал signal(МожноПисать), разрешающий работать первому Писателю из очереди заблокированных (если таковой имеется). Если заблокированных Писателей нет, то действие воспринимается как пустой оператор:

procedure КонецЧтения; begin

Читатели := Читатели - 1;

16

if (Читатели = 0) then signal(МожноПисать);

end;

Перед началом записи Писателем следует проверить, есть ли работающие Читатели или Писатели, и разрешить доступ только при отсутствии таковых. В случае истинности условия (Читатели > 0) or (КтоТоПишет) Писатель блокируется и ставится в очередь, связанную с переменной МожноПисать. Понятно, что значение этого условия может быть «ложь», т. е. доступ к Фонду открыт. Тогда устанавливается флаг КтоТоПишет = true, анализ которого в процедуре НачалоЗаписи закрывает доступ Читателю. Как, впрочем, и повторный вызов НачалоЗаписи другим Писателем приведет к его блокировке. Другим путем установки этого флага является посылка сигнала signal(МожноПисать) из другой задачи, что продолжит работу процедуры НачалоЗаписи с действия, следующего за оператором wait(МожноПисать), т. е. опять же с установки этого флага: procedure НачалоЗаписи;

begin

if (Читатели > 0) or (КтоТоПишет) then wait(МожноПисать);

КтоТоПишет := true; end;

И, наконец, процедура КонецЗаписи: procedure КонецЗаписи;

begin

КтоТоПишет := false;

if (check(МожноЧитать) > 0) then signal(МожноЧитать)

else

if (check(МожноПисать) > 0) then signal(МожноПисать);

end;

Писатель, закончивший запись, первым делом сбрасывает флаг КтоТоПишет = false. Далее проверяется наличие заблокированных Читателей, и если таковые имеются, то посылается сигнал signal(МожноЧитать), который «освобождает» первого Читателя из очереди, связанной с переменной МожноЧитать, заблокированного действием

17

wait(МожноЧитать) (см. процедуру НачалоЧтения, действие, помеченное (*)). А далее Читатели последовательно освобождают друг друга действием signal(МожноЧитать) (оператор **). Таким образом, могут работать несколько Читателей.

Важным обстоятельством является тот факт, что монитор в данном примере обеспечивает взаимное исключение доступа к описанным в нем процедурам и данным. Но не менее важен и другой факт. В приведенном примере намеренно не рассматривалась возможность ситуации, когда за временной интервал после посылки сигнала одной задачей (вызовом signal) условие блокировки другой задачи (вызовом wait) изменилось в результате действий какой-то третьей задачи. Просто подразумевалось, что в данном примере предполагается монитор с реализацией упомянутой ранее «семантики Хоара», чем обеспечивается немедленное возобновление ждущей процедуры после посылки ей сигнала.

4. УСЛОВНАЯ ПЕРЕМЕННАЯ

Механизм условной переменной как особенность монитора представляет собой настолько удачную конструкцию, что в настоящее время реализуется как самостоятельное средство синхронизации в многозадачных операционных системах. Как и в мониторе, данный механизм предполагает наличие объекта типа condition, над которым определены две операции wait и signal. На практике условная переменная может восприниматься как изначально пустая очередь заблокированных задач, которые ожидают выполнения определенного условия; но эта очередь не видна ни ждущим, ни сигнализирующим задачам. Типовую схему использования условной переменной можно представить следующим образом:

cond : condition; m : mutex;

«Ожидающая» задача: lock(m);

while (условие) wait(cond, m);

<Действия критической секции> unlock(m);

«Сигнализирующая» задача: lock(m);

18

условие := false; signal (cond); unlock(m);

Наличие мьютекса здесь обусловливается тем, что действия над условной переменной должны выполняться в участке кода с обеспеченным взаимным исключением. Поэтому с «переменной» cond связывается мьютекс, обеспечивающий взаимное исключение. Реализация такого механизма синхронизации осуществляется на уровне операционной системы, функции wait(cond, m) и signal(cond) обычно реализуются в ядре. Отметим, что при определении условной переменной в рамках монитора во мьютексе не было необходимости, так как взаимное исключение обеспечивалось самим монитором. Поэтому функция wait(cond) имеет только один параметр, указывающий на переменную, по которой осуществляется блокировка. В реализации самостоятельного механизма условной переменной необходимо также указывать на мьютекс – wait(cond, m), обеспечивающий взаимное исключение.

Семантика условной переменной подразумевает несколько особенностей, которые диктуются реализацией, и заслуживает более детального рассмотрения. Пусть «Ожидающая задача» выполнила lock(m) и захватила мьютекс, при этом пусть условие = true. Тогда будет вызвана функция wait(cond, m), которая заблокирует эту задачу и поставит ее в очередь, связанную с условной переменной cond. Здесь проявляется первая, казалось бы странная, особенность – мьютекс m освобождается, что позволяет «Сигнализирующей задаче» (или, в общем случае, какой-либо другой задаче) захватить его, стать его новым владельцем и начать выполнять действия, в результате которых можно будет изменить значение условия. «Сигнализирующая задача» вызывает функцию signal(cond), которая посылает сигнал «Ожидающей задаче», что должно активизировать ее и позволить приступить к выполнению кода критической секции. Точнее говоря, переместить ее из очереди задач, заблокированных на условной переменной cond, в очередь операционной системы для задач, готовых к выполнению. А уж когда приступить к выполнению критической секции, «решит» операционная система на основе заложенных в нее алгоритмов планирования. Однако тут проявляется вторая особенность. Эти действия произойдут только после освобождения мьютекса «Сигнализирующей

19

задачей» – вызова функции unlock(m) – и смены его владельца, которым снова станет «Ожидающая задача».

И наконец, обратим внимание на то, что после «получения сигнала» ожидающая задача выполнит еще одну проверку условия цикла while(условие). И это правильно, так как ничто не мешает какой-то третьей задаче изменить значение условия на true за время манипуляций с «передачей сигнала» начиная от момента вызова «Сигнализирующей задачей» функции unlock(m) и заканчивая возобновлением действий «Ожидающей задачи». Как уже отмечалось, использование в этой ситуации оператора цикла, а не оператора условного перехода является необходимым приемом.

5. РЕАЛИЗАЦИЯ МОНИТОРА В JAVA

Попытки реализации идеи монитора в языках высокого уровня предпринимались несколько раз. В середине 70-х гг. ХХ в. датский ученый Бринч Хансен разработал язык программирования Concurrent Pascal с целью предоставить инструмент для структурированного программирования операционных систем. Этот язык является расширением общеизвестного сейчас «последовательного» Pascal Никлауса Вирта с дополнением его возможностями параллельного программирования, включающими в себя процессы и мониторы. По-видимому, эта разработка может считаться первой, где такой механизм синхронизации был предоставлен разработчику на уровне языка высокого уровня. Далее следовало бы выделить язык программирования Modula-2, автором которого является тот же Никлаус Вирт. Элегантность этой разработки, популярность которой в настоящее время незаслуженно отошла на второй план, трудно переоценить. Следует также сказать о языке программирования Mesa, в котором предоставляется возможность создавать параллельные процессы и осуществлять синхронизацию посредством мониторов. Однако эти разработки (как, впрочем, и около десятка других) не получили такой широкой популярности, как Java и С# (реализованный по образу и подобию Java в фирме Microsoft).

Язык программирования Java и интерпретирующая виртуальная машина Java, позволяющие реализовать многозадачность с поддержкой монитора в качестве механизма синхронизации, были разработаны в конце 90-х гг. группой программистов фирмы Sun Microsystems. Остановимся немного подробнее на этой разработке.

20

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]