Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
15-21.docx
Скачиваний:
3
Добавлен:
01.04.2025
Размер:
769 Кб
Скачать
☆

21. Мониторы.

стр.170

Чтобы облегчить написание безошибочных программ, Бринч Хансен (Brinch Hansen) в 1973 году и Хоар (Ноаге) в 1974 году предложили высокоуровневый синхронизационный примитив, названный монитором. Их предложения мало отличались друг от друга. Монитор представляет собой коллекцию переменных и структур данных, сгруппированных вместе в специальную разновидность модуля или пакета процедур. Процессы могут вызывать любые необходимые им процедуры, имеющиеся в мониторе, но они не могут получить непосредственный доступ к внутренним структурам данных монитора из процедур, объявленных за пределами монитора. У мониторов имеется весьма важное свойство, позволяющее успешно справляться со взаимными исключениями: в любой момент времени в мониторе может быть активен только один процесс. Мониторы являются конструкцией языка программирования, поэтому компилятор осведомлен об их особенностях и способен обрабатывать вызовы процедур монитора не так, как вызовы всех остальных процедур. Обычно при вызове процессом процедуры монитора первые несколько команд процедуры осуществляют проверку на текущее присутствие активности других процессов внутри монитора. Если какой-нибудь другой процесс будет активен, вызывающий процесс будет приостановлен до тех пор, пока другой процесс не освободит монитор. Если монитор никаким другим процессом не используется, вызывающий процесс может в него войти. Реализация взаимного исключения при входе в монитор возлагается на компилятор, но чаще всего для этого используется мьютекс или двоичный семафор. Поскольку обеспечением взаимного исключения занимается компилятор, а не программист, вероятность того, что будут допущены какие-то неправильные действия, становится гораздо меньше. В любом случае тот, кто создает монитор, не должен знать, как именно компилятор обеспечивает взаимное исключение. Достаточно лишь знать, что после превращения всех критических областей в процедуры монитора никакие два процесса не будут выполнять код своих критических областей в одно и то же время. Хотя мониторы и обеспечивают простой способ достижения взаимного исключения, этого недостаточно. Также нужен способ, позволяющий заблокировать процессы, которые не в состоянии продолжить свою работу. В случае с решением задачи производителя-потребителя проще всего поместить все тесты на заполненность и опустошенность буфера в процедуры монитора, но как тогда производитель должен заблокироваться, обнаружив, что буфер заполнен до отказа? Для решения этой проблемы нужно ввести условные переменные, а также две проводимые над ними операции — wait и signal. Когда процедура монитора обнаруживает невозможность продолжения своей работы (например, производитель обнаружил, что буфер заполнен), она осуществляет операцию wait в отношении какой-нибудь условной переменной, скажем, full. Это действие призывает процесс к блокированию. Оно также позволяет войти в монитор другому процессу, которому ранее этот вход был запрещен. Этот другой процесс, например потребитель, может активизировать работу своего приостановленного партнера, осуществив операцию signal в отношении условной переменной, изменения значения которой ожидает его партнер. Чтобы в мониторе в одно и то же время не находились сразу два активных процесса, нам необходимо правило, предписывающее дальнейшее развитие событий после осуществления операции signal. Xoap предложил позволить только что активизированному процессу приостановить работу другого процесса. Бринч Хансен предложил разрешить проблему, потребовав обязательного и немедленного выхода из монитора того процесса, который осуществил операцию signal. Иными словами, операция signal должна фигурировать только в качестве завершающей операции в процедуре монитора. Мы воспользуемся предложением Бринча Хансена, поскольку оно проще по замыслу и по реализации. Если операция signal осуществляется в отношении условной переменной, изменения которой ожидает сразу несколько процессов, активизирован будет лишь один из них, определяемый системным планировщиком. В резерве есть еще одно, третье решение, которое не было предложено ни Хоаром, ни Бринчем Хансеном. Суть его в том, чтобы позволить сигнализирующему процессу продолжить свою работу и позволить ожидающему процессу возобновить работу только после того, как сигнализирующий процесс покинет монитор. Условные переменные не являются счетчиками. Они не аккумулируют сигналы для последующего использования, как это делают семафоры. Поэтому если сигнал обрабатывает условную переменную, изменения которой никто не ожидает, этот сигнал теряется навсегда. Иными словами, операция wait должна предшествовать операции signal. Это правило значительно упрощает реализацию. На практике проблем не возникает, поскольку куда проще, если понадобится, отследить состояние каждого процесса с переменными. Процесс, который мог бы при других условиях осуществить операцию signal, может увидеть, взглянув на переменные, что в ней нет необходимости.

Может создаться впечатление, что операции wait и signal похожи на операции sleep и wakeup, которые приводят к фатальному состоянию состязательности. Конечно же, они очень похожи, но с одной весьма существенной разницей: sleep и wakeup терпели неудачу, когда один процесс пытался заблокироваться, а другой пытался его активизировать. При использовании мониторов такая ситуация исключена. Автоматическая организация взаимного исключения в отношении процедур монитора гарантирует следующее: если, скажем, производитель, выполняя процедуру внутри монитора, обнаружит, что буфер полон, он будет иметь возможность завершить операцию wait, не испытывая волнений о том, что планировщик может переключиться на выполнение процесса потребителя еще до того, как операция wait будет завершена. Потребителю вообще даже не будет позволено войти в монитор, пока не завершится операция wait, и производитель не будет помечен как неспособный к дальнейшему продолжению работы. Хотя Pidgin Pascal является воображаемым языком, некоторые реально существующие языки программирования также поддерживают мониторы, быть может, и не всегда в форме, разработанной Хоаром и Бринчем Хансеном. Одним из таких языков является Java — объектно-ориентированный язык, поддерживающий потоки на уровне пользователя, а также разрешающий группировать методы (процедуры) в классы. При добавлении к объявлению метода ключевого слова synchronized Java гарантирует следующее: как только один поток приступает к выполнению этого метода, то ни одному другому потоку не будет позволено приступить к выполнению любого другого синхронизированного метода этого объекта. Решение задачи производителя-потребителя с использованием мониторов, реализованное на языке Java включает в себя четыре класса. Самый первый класс, ProducerConsumer, создает и запускает два потока р и с. Второй и третий классы — producer и consumer — содержат, соответственно, код для производителя и потребителя. И наконец, класс our_monitor представляет собой монитор. Он состоит из двух синхронизированных потоков, которые используются для фактического помещения записей в общий буфер и извлечения их оттуда. Потоки производителя и потребителя функционально идентичны, у производителя имеется бесконечный цикл, генерирующий данные и помещающий их в общий буфер. У потребителя есть аналогичный бесконечный цикл, извлекающий данные из общего буфера и производящий над ними некие полезные действия. Интересующей нас частью этой программы является класс 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 — это всего лишь способ приостановки работы.

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