Материал: Sb97573

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

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

Источник: https://studfile.net/preview/16438914/