Мониторы

Если вы думаете, что благодаря семафорам и мьютексам организация взаимодействия процессов кажется весьма простой задачей, то выкиньте это из головы. Присмотритесь к порядку выполнения операций down перед вставкой записей в буфер или удалением записей из буфера, показанному в листинге 2.6.
Допустим, что две процедуры down в коде производителя были переставлены местами, чтобы значение mutex было уменьшено до уменьшения значения empty, а не после него. Если бы буфер был заполнен под завязку, производитель был бы заблокирован, а значение mutex было бы установлено в 0. Следовательно, при следующей попытке доступа производителя к буферу он осуществлял бы down в отношении mutex, который теперь имеет значение 0, и также блокировал бы его. Оба процесса находились бы в заблокированном состоянии бесконечно долго, не позволяя что-либо сделать. Такая неприятная ситуация называется взаимной блокировкой, к ее детальному рассмотрению мы вернемся в главе 6.

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

Чтобы облегчить написание безошибочных программ, Бринч Хансен (Brinch Hansen) в 1973 году и Хоар (Hoare) в 1974 году предложили высокоуровневый синхронизационный примитив, названный монитором. Их предложения, как мы увидим дальше, мало отличались друг от друга. Монитор представляет собой коллекцию переменных и структур данных, сгруппированных вместе в специальную разновидность модуля или пакета процедур. Процессы могут вызывать любые необходимые им процедуры, имеющиеся в мониторе, но не могут получить непосредственный доступ к внутренним структурам данных монитора из процедур, объявленных за пределами монитора. В листинге 2.9 показан монитор, написанный на воображаемом языке Pidgin Pascal. Язык C здесь не подойдет, поскольку мониторы являются понятиями языка, а C такими понятиями не обладает.

Листинг 2.9. Монитор

monitor example integer i; condition c;

procedure producer();

end;

procedure consumer();

end;

end monitor;

У мониторов имеется весьма важное свойство, позволяющее успешно справляться со взаимными исключениями: в любой момент времени в мониторе может быть активен только один процесс. Мониторы являются конструкцией языка программирования, поэтому компилятор осведомлен об их особенностях и способен обрабатывать вызовы процедур монитора не так, как вызовы всех остальных процедур. Обычно при вызове процессом процедуры монитора первые несколько команд процедуры осуществляют проверку на текущее присутствие активности других процессов внутри монитора. Если какой-нибудь другой процесс будет активен, вызывающий процесс будет приостановлен до тех пор, пока другой процесс не освободит монитор. Если монитор никаким другим процессом не используется, вызывающий процесс может в него войти.

Реализация взаимного исключения при входе в монитор возлагается на компилятор, но чаще всего для этого используется мьютекс или двоичный семафор. Поскольку обеспечением взаимного исключения занимается компилятор, а не программист, вероятность того, что будут допущены какие-то неправильные действия, становится гораздо меньше. В любом случае тот, кто создает монитор, не должен знать, как именно компилятор обеспечивает взаимное исключение. Достаточно лишь знать, что после превращения всех критических областей в процедуры монитора никакие два процесса не будут выполнять код своих критических областей в одно и то же время.

Хотя мониторы и обеспечивают простой способ достижения взаимного исключения, как мы видели ранее, этого недостаточно. Нам также нужен способ, позволяющий заблокировать процессы, которые не в состоянии продолжить свою работу. В случае с решением задачи производителя-потребителя проще всего поместить все тесты на заполненность и опустошенность буфера в процедуры монитора, но как тогда производитель должен заблокироваться, обнаружив, что буфер заполнен до отказа?

Для решения этой проблемы нужно ввести условные переменные, а также две проводимые над ними операции — wait и signal. Когда процедура монитора обнаруживает невозможность продолжения своей работы (например, производитель обнаружил, что буфер заполнен), она осуществляет операцию wait в отношении какой-нибудь условной переменной, скажем, full. Это действие призывает процесс к блокированию. Оно также позволяет войти в монитор другому процессу, которому ранее этот вход был запрещен. Мы уже встречались с условными переменными и с этими операциями, когда рассматривали пакет Pthreads.

Этот другой процесс, например потребитель, может активизировать работу своего приостановленного партнера, осуществив операцию signal в отношении условной переменной, изменения значения которой ожидает его партнер. Чтобы в мониторе в одно и то же время не находились сразу два активных процесса, нам необходимо правило, предписывающее дальнейшее развитие событий после осуществления операции signal. Хоар предложил позволить только что активизированному процессу приостановить работу другого процесса. Бринч Хансен предложил разрешить проблему, потребовав обязательного и немедленного выхода из монитора того процесса, который осуществил операцию signal. Иными словами, операция signal должна фигурировать только в качестве завершающей операции в процедуре монитора. Мы воспользуемся предложением Бринча Хансена, поскольку оно проще по замыслу и реализации. Если операция signal осуществляется в отношении условной переменной, изменения которой ожидают сразу несколько процессов, активизирован будет лишь один из них, определяемый системным планировщиком.

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

Условные переменные не являются счетчиками. Они не аккумулируют сигналы для последующего использования, как это делают семафоры. Поэтому, если сигнал обрабатывает условную переменную, изменения которой никто не ожидает, этот сигнал теряется навсегда. Иными словами, операция wait должна предшествовать операции signal. Это правило значительно упрощает реализацию. На практике проблем не возникает, поскольку куда проще, если понадобится, отследить состояние каждого процесса с переменными. Процесс, который мог бы при других условиях осуществить операцию signal, может увидеть, взглянув на переменные, что в ней нет необходимости.

Схема решения задачи производителя-потребителя с использованием мониторов показана на воображаемом языке Pidgin Pascal в листинге 2.10. Здесь преимущества использования Pidgin Pascal проявляются в его простоте и точном следовании модели Хоара — Бринча Хансена.

Листинг 2.10. Набросок решения задачи производителя-потребителя с помощью мониторов. В любой момент времени может быть активна только одна процедура монитора.

В буфере содержится N мест monitor ProducerConsumer condition full, empty; integer count;

procedure insert(item: integer); begin

if count = N then wait(full); insert item(item); count := count + 1; if count =1 then signal(empty)

end;

function remove : integer; begin

if count =0 then wait(empty);

remove = remove item;

count := count - 1;

if count = N- 1 then signal(full)

end;

count := 0; end monitor;

procedure producer; begin

while true do begin

item = produce item;

ProducerConsumer.insert(item)

End

end;

procedure consumer; begin

while true do begin

item = ProducerConsumer.remove; consume item(item)

end

end;

Может создаться впечатление, что операции wait и signal похожи на операции sleep и wakeup, которые, как мы видели ранее, приводят к фатальному состоянию состязательности. Конечно же, они очень похожи, но с одной весьма существенной разницей: sleep и wakeup терпели неудачу, когда один процесс пытался заблокироваться, а другой — его активизировать. При использовании мониторов такая ситуация исключена. Автоматическая организация взаимного исключения в отношении процедур монитора гарантирует следующее: если, скажем, производитель, выполняя процедуру внутри монитора, обнаружит, что буфер полон, он будет иметь возможность завершить операцию wait, не испытывая волнений о том, что планировщик может переключиться на выполнение процесса потребителя еще до того, как операция wait будет завершена. Потребителю вообще не будет позволено войти в монитор, пока не завершится операция wait и производитель не будет помечен как неспособный к дальнейшему продолжению работы.

Хотя Pidgin Pascal является воображаемым языком, некоторые реально существующие языки программирования также поддерживают мониторы, быть может, и не всегда в форме, разработанной Хоаром и Бринчем Хансеном. Одним из таких языков является Java — объектно-ориентированный язык, поддерживающий потоки на уровне пользователя, а также разрешающий группировать методы (процедуры) в классы. При добавлении к объявлению метода ключевого слова synchronized Java гарантирует следующее: как только один поток приступает к выполнению этого метода, ни одному другому потоку не будет позволено приступить к выполнению любого другого метода этого объекта с ключевым словом synchronized. Без использования ключевого слова synchronized гарантии чередования отсутствуют.

В листинге 2.11 приведено решение задачи производителя-потребителя с использованием мониторов, реализованное на языке Java. Решение включает в себя четыре класса. Самый первый класс, ProducerConsumer, создает и запускает два потока — p и c. Второй и третий классы — producer и consumer — содержат код для производителя и потребителя соответственно. И наконец, класс our_monitor представляет собой монитор. Он состоит из двух синхронизированных потоков, которые используются для фактического помещения записей в общий буфер и извлечения их оттуда. В отличие от предыдущего примера, здесь мы наконец-то приводим полный код методов insert и remove.

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

Интересующей нас частью этой программы является класс our_monitor, который содержит буфер, управляющие переменные и два синхронизированных метода. Когда производитель активен внутри метода insert, он знает наверняка, что потребитель не может быть активен внутри метода remove, что позволяет обновлять переменные и буфер без опасений создать условия для состояния состязательности. В переменной count отслеживается количество записей, находящихся в буфере. Она может принимать любые значения от 0 и до N - 1 включительно. Переменная lo является индексом места в буфере, откуда будет извлечена следующая запись. Аналогично этому переменная hi является индексом места в буфере, куда будет помещена следующая запись. Допустимо равенство lo = hi, которое означает, что в буфере находится либо 0, либо N записей. Значение count указывает на суть создавшейся ситуации.

Синхронизированные методы в Java существенно отличаются от классических мониторов: в Java отсутствуют встроенные условные переменные. Вместо них этот язык предлагает две процедуры, wait и notify, которые являются эквивалентами sleep и wakeup, за исключением того, что при использовании внутри синхронизированных методов они не могут попасть в состязательную ситуацию. Теоретически метод wait может быть прерван, для чего, собственно, и предназначен весь окружающий его код. Java требует, чтобы обработка исключений проводилась в явном виде. В нашем случае нужно просто представить, что использование метода go_to_sleep — это всего лишь способ приостановки работы.

Благодаря автоматизации взаимного исключения входа в критические области мониторы (по сравнению с семафорами) позволяют снизить уровень ошибок при параллельном программировании. Тем не менее у них тоже имеется ряд недостатков. Недаром оба наших примера мониторов приведены на языке Pidgin Pascal, а не на C, как все другие примеры в этой книге. Как уже упоминалось, мониторы являются понятием языка программирования. Компилятор должен их распознать и каким-то образом устроить взаимное исключение. C, Pascal и большинство других языков не имеют мониторов, поэтому не имеет смысла ожидать от их компиляторов реализации каких-нибудь правил взаимного исключения. И действительно, как компилятор сможет узнать, какие процедуры были в мониторах, а какие нет?

В этих языках нет и семафоров, но их легко добавить: нужно всего лишь дополнить библиотеку двумя короткими подпрограммами на ассемблере, чтобы получить системные вызовы up и down. Компиляторам даже не нужно знать, что они существуют. Разумеется, операционная система должна знать о семафорах, но во всяком случае, если у вас операционная система, использующая семафоры, вы можете создавать пользовательские программы для нее на C или С++ (или даже на ассемблере, если вам настолько нечем больше заняться). Для использования мониторов нужен язык, в который они встроены.

Другая особенность, присущая мониторам и семафорам, состоит в том, что они разработаны для решения проблем взаимного исключения при работе с одним или несколькими центральными процессорами, имеющими доступ к общей памяти. Помещая семафоры в общую память и защищая их командами TSL или XCHG, мы можем избежать состязательной ситуации. При переходе к распределенной системе, состоящей из связанных по локальной сети нескольких центральных процессоров, у каждого из которых имеется собственная память, эти примитивы становятся непригодными. Следует сделать вывод, что семафоры имеют слишком низкоуровневую природу, а мониторы бесполезны, за исключением небольшого количества языков программирования. К тому же ни один из примитивов не позволяет осуществлять информационный обмен между машинами. Здесь нужно что-то иное.

2.3.8.

<< | >>
Источник: Э. ТАНЕНБАУМ Х. БОС. СОВРЕМЕННЫЕ ОПЕРАЦИОННЫЕ СИСТЕМ Ы 4-е ИЗДАНИЕ. 2015

Еще по теме Мониторы:

  1. Монитор
  2. 2.2.1. Первое измерение: монитор отклонения
  3. Видеокомплекс
  4. Методы воплощения на телевидении
  5. Передвижная телевизионная станция
  6. 4.4. Взгляд в будущее
  7. 4. Интернет.
  8. Э. ТАНЕНБАУМ, А. ВУДХАЛЛ. ОПЕРАЦИОННЫЕ СИСТЕМЫ Разработка и реализация 3-е издание, 2007
  9. Как осуществляется телевизионная передача
  10. Стикеры
  11. 7.7. МОДЕЛЬ АКВАРИУМА
  12. 2.1. Зона бессознательного