Мониторы
Монитор представляет собой набор информационных структур и процедур, которые используются в режиме разделения. Некоторые из этих процедур являются внешними и доступны процессам пользователей, их имена представляют входные точки монитора. Пользователь не имеет доступа к информационным структурам монитора и может воздействовать на них, только обращаясь ко входным точкам. Монитор, таким образом, воплощает принцип инкапсуляции данных. В терминах объектно-ориентированного программирования ресурс, обслуживаемый монитором, представляет собой объект, а входные точки - методы работы с этим объектом. Особенностью монитора, однако, является то, что в его состав входят, так называемые, "процедуры с охраной", которые не могут выполняться двумя процессами одновременно.
Не являясь средством более мощным или гибким, чем рассмотренные выше примитивы, мониторы, однако, представляют значительно более удобный инструмент для программиста, избавляя его от необходимости формировать критические секции, обеспечивая более высокий уровень интеграции данных и предупреждая возможные ошибки во взаимном исключении.
Если в задаче "производители-потребители" процессы программируются пользователем, то вид этих процессов может быть таким:
1 #include <monitor.h> 2 /*== процесс-производитель (может быть отдельным модулем) ==*/ 3 void producer ( void ) { 4 portion work; 5 while (1) { 6 < производство порции в work > 7 putPortion ( &work ); 8 } 9 } 10 /*== процесс-потребитель (может быть отдельным модулем) ==*/ 11 void consumer ( void ) { 12 portion work; 13 while (1) { 14 getPortion ( &work ); 15 < обработка порции в work> 16 } 17 }
Обратим внимание на то, что процессы, во-первых, никоим образом не заботятся о разделении данных, во-вторых, не используют никакие общие данные. Такая "беззаботная" работа процессов, однако, должна быть поддержана монитором, входные точки которого описаны в файле monitor.h, а определение его имеет такой вид:
1 /*== монитор производителей-потребителей (отдельный модуль) ==*/ 2 #define BSIZE ... 3 /* буфер */ 4 static portion buffer [BSIZE]; 5 /* индексы буфера для чтения и записи*/ 6 static int rIndex = 0, wIndex = 0; 7 /* счетчик заполнения */ 8 static int cnt = 0; 9 /* события НЕ_ПУСТ, НЕ_ПОЛОН */ 10 static event nonEmpty, nonFull; 11 /*== процедура занесения порции в буфер ==*/ 12 void guard putPortion ( portion *x ) { 13 /* если буфер полон - ожидать события НЕ_ПОЛОН */ 14 if ( cnt == BSIZE ) wait (nonFull); 15 /* запись порции в буфер */ 16 memcpy ( buffer + wIndex, x, sizeof(portion) ) ; 17 /* модификация индекса записи */ 18 if ( ++wIndex == BSIZE ) wIndex = 0; 19 cnt++; /* подсчет порций в буфере */ 20 /* сигнализация о том, 21 что буфер НЕ_ПУСТ */ 22 signal (nonEmpty); 23 } 24 /*== процедура позучения порции из буфера ==*/ 25 void guard getPortion ( portion *x ) { 26 if ( cnt == 0 ) wait (nonEmpty); 27 memcpy ( x, buffer + rIndex, sizeof(portion) ) ; 28 if ( ++rIndex == BSIZE ) rIndex = 0; 29 cnt++; 30 signal (nonFull); 31 }
В реализации монитора нам пришлось прибегнуть к некоторым новым обозначениям. Во-первых, функции монитора даны с описателем guard (охрана). Это означает, что они должны выполняться в режиме взаимного исключения. В литературе часто употребляется образное сравнение мониторов с комнатой, в которой может находиться только один человек. Такая комната показана на Рисунке 8.2. Если человек (процесс) желает войти в комнату (охраняемую процедуру монитора), то он становится во входную очередь к двери 1, в которой он ожидает (блокируется) до тех пор, пока комната (монитор) не освободится. Дверь 1 (вход) отпирается только в том случае, если комната пуста, пропускает только одного человека и запирается за ним. Дверь 2 (выход) не заперта, когда она открывается, отпирается и дверь 1.
Рис. 8.2. Простая модель монитора |
Обратите внимание на то, что взаимное исключение обеспечивается для всех охраняемых процедур, а не только для одноименных. В сущности, такая процедура представляет собой ту же критическую секцию, и ее охрана реализуется любым из методов защиты критической секции, скорее всего, в роли "охранника" будет выступать скрытый семафор.
Другие наши нововведения связаны с блокировками внутри охраняемой процедуры. Мы ввели тип данных, названный нами event. Этот тип представляет некоторое событие. Примитив wait проверяет наступление этого события и переводит процесс в ожидание, если событие еще не произошло. Примитив signal сигнализирует о наступлении события. Событие является потребляемым ресурсом: если два процесса ожидают одного и того же события, то при наступлении события разблокирован будет только один из процессов, другой будет вынужден ждать повторного наступления такого же события.
Примитивы ожидания-сигнализации требуют принятия решений по ряду проблем их реализации. Если один процесс выдает сигнал о наступлении некоторого события, то в какой момент должен быть разблокирован процесс, ожидающий этого события? Если немедленно, то тогда в нашей "комнате" окажется два процесса одновременно: разблокированный процесс и процесс, выдавший сигнал, но еще не покинувший монитор.
Если позже, то за это время в монитор может войти какой-то третий процесс. Если несколько процессов ожидают одного и того же события, то какой (или какие) из них должен быть разблокирован? Если в момент выдачи сигнала нет процессов, ожидающих этого события, то должен ли сигнал сохраняться или его можно "потерять"?
Общий подход к решению этой проблемы иллюстрируется расширением модели "одноместной комнаты", показанным на Рисунке 8.3. Для каждого события, которое может ожидаться в мониторе, мы водим свою очередь с соответствующими входными и выходными дверями для нее. На рисунке эти очереди показаны внизу монитора. Мы вводим также очередь, которую мы называем приоритетной. Процессы, находящиеся в очередях, не считаются находящимися в мониторе. "Правила для посетителей" комнаты-монитора следующие.
- Новый процесс поступает во входную очередь. Новый процесс может войти в монитор через дверь 1 только, если в мониторе нет других процессов.
- Если процесс выходит из монитора через дверь 2 (выход), то в монитор входит процесс из двери 4 - из приоритетной очереди. Если в приоритетной очереди нет ожидающих, входит процесс из двери 1 (если есть ожидающие за ней).
- Процесс, выполняющий операцию wait, выходит из монитора в дверь, ведущую в соответствующую очередь (5 или 7).
- Если процесс выполняет операцию signal, то проверяется очередь, связанная с событием. Если эта очередь непуста, то сигнализирующий процесс уходит в приоритетную очередь (дверь 3), а в монитор входит один процесс из очереди к событию (дверь 6 или 8). Если очередь пуста, сигнализирующий процесс остается в мониторе.
- Все очереди обслуживаются по дисциплине FCFS.
Рис.8.3. Расширенная модель монитора |
Эти правила предполагают, что процесс будет разблокирован немедленно (речь идет не о немедленной активизации процесса, а о его разблокировании - перемещении в очередь готовых к выполнению). Разблокированный процесс имеет преимущество перед процессом, ожидающим во входной очереди.
В нашем примере производителей-потребителей мы употребляли операцию signal в конце процедуры.
Такое употребление характерно для очень большого числа задач. Наши правила требуют перемещения сигнализирующего процесса в приоритетную очередь. Однако, если сигнализирующий процесс после выдачи сигнала больше не выполняет никаких действий с разделяемыми данными, необходимости в таком перемещении (и вообще в приоритетной очереди) нет. Жесткая привязка сигнала к окончанию охраняемой процедуры снижает гибкость монитора, но значительно упрощает диспетчеризацию процессов в мониторе.
Возможно решение, в котором операция signal разблокирует все процессы, находящиеся в соответствующей очереди. Поскольку все ожидавшие процессы не могут вместе войти в монитор, в нем остается только один из них, успевший "подхватить" событие, а остальные направляются в приоритетную очередь. Процесс, который разблокировался таким образом, уже не может, однако, быть уверенным в том, что его разблокирование гарантирует наступление события (событие могло быть перехвачено другим процессом). Поэтому в проверке условия ожидания оператор if для такой реализации должен быть заменен оператором while, например, строки 11, 12 последнего примера должны выглядеть так:
11 / если буфер полон - ожидать события НЕ_ПОЛОН */ 12 while ( cnt == BSIZE ) wait (nonFull);
Поскольку охраняемые процедуры есть критические секции, для поддержания высокого уровня мультипрограммирования нахождение в них процессов должно быть кратковременным. Проблема времени может возникнуть в том случае, когда в процедуре монитора имеется обращение к другому монитору. По нашим правилам такое обращение не выводит процесс из первого монитора. Однако во втором (вложенном) мониторе процесс может быть заблокирован, тогда его пребывание в первом мониторе недопустимо затянется. Возможно несколько вариантов решения этой проблемы:
- переложить ответственность за возникновение такой ситуации на программиста;
- запретить вложенные вызовы вообще;
- при вхождении во вложенный монитор автоматически снимать исключение с внешнего монитора, когда процесс возвращается из вложенного монитора, он попадает в приоритетную очередь внешнего монитора;
- предоставить программисту выбор из перечисленных выше возможностей.
Содержание раздела