
- •Лекція №8 Паралельні обчислення: семафори, монітори, повідомлення
- •8.1. Семафоры
- •8.1.1. Взаимные исключения
- •Листинг 8.2. Взаимоисключения с использованием семафоров
- •8.1.2. Задача “производителя-потребителя” ("писатель-читатель")
- •8.2. Мониторы
- •8.2.1. Мониторы с сигналами (мониторы Хоара)
- •8.2.2. Мониторы с оповещением и широковещанием
- •8.3.1. Синхронизация
- •8.3.2. Адресация
- •8.3.3. Формат сообщения
8.2. Мониторы
Межпроцессное взаимодействие с использованием семафоров выглядит довольно просто. Но это кажущаяся простота. Семафоры обеспечивают достаточно мощный и гибкий инструмент для осуществления взаимных исключений и координации процессов. Однако создать корректно работающую программу с использованием семафоров не всегда легко. Сложность заключается в том, что операции wait и signal могут быть разбросаны по всей программе, и не всегда можно отследить их взаимодействие на контролируемые ими семафоры. Использовать семафоры нужно очень аккуратно и осторожно, так как одна незначительная ошибка (например, как мы уже знаем - простая перестановка двух операций wait) может привести к останову системы. Это напоминает программирование на ассемблере, но на самом деле еще сложнее.
Для того чтобы упростить и облегчить работу программистов при написании корректных программ, было предложено более высокоуровневое средство синхронизации, называемое монитором. Монитор - это набор процедур, переменных и структур данных, объединенных в особый модуль или пакет. Процессы могут вызывать процедуры монитора, но не имеют доступа к внутренним данным монитора.
Монитор представляет собой конструкцию языка программирования, которая обеспечивает функциональность, эквивалентную функциональности семафоров, но легче управляется. Мониторы имеют важное свойство, которое делает их полезными для достижения взаимного исключения: только один процесс может быть активным по отношению к монитору.
Компилятор обрабатывает вызовы процедур монитора особым образом. Обычно, когда процесс вызывает процедуру монитора, то первые несколько инструкций этой процедуры проверяют, не активен ли какой-либо другой процесс по отношению к этому монитору. Если да, то вызывающий процесс приостанавливается, пока другой процесс не освободит монитор. Таким образом, исключение входа нескольких процессов в монитор реализуется не программистом, а компилятором, что делает ошибки менее вероятными.
Впервые формальное определение концепции монитора с некоторыми отличиями было дано в 1974 году Хоаром (Hoare) и Бринч Хансеном (Brinch Hansen). Рассмотрим монитор версии Хоара.
8.2.1. Мониторы с сигналами (мониторы Хоара)
Монитор представляет собой программный модуль, состоящий из инициализирующих последовательностей, одной или нескольких процедур и локальных данных. Основные его характеристики.
Локальные переменные монитора доступны только его процедурам.
Процесс входит в монитор путем вызова одной из его процедур.
В мониторе в определенный момент времени может выполняться только один процесс; любой другой процесс, вызывающий монитор, будет приостановлен в ожидании доступности монитора.
Первые две характеристики заставляют нас вспомнить об объектах в ООП. Фактически объектно-ориентированные языки программирования могут легко организовать монитор как объект со специальными характеристиками.
Соблюдение условия выполнения только одного процесса в определенный момент времени позволяет монитору обеспечить взаимоисключения. Данные монитора доступны в этот момент только одному процессу, следовательно, защитить совместно используемые структуры данных можно, просто поместив их в монитор. Если данные в мониторе представляют некий ресурс, то монитор обеспечивает взаимоисключение при обращении к ресурсу.
На абстрактном уровне можно описать структуру монитора следующим образом:
monitor monitor_name {
описание внутренних переменных ;
void m1(...){...
}
void m2(...){...
}
...
void mn(...){...
}
{
блок инициализации
внутренних переменных;
}
}
Здесь функции m1,..., mn представляют собой функции-методы монитора, а блок инициализации внутренних переменных содержит операции, которые выполняются один и только один раз: при создании монитора или при самом первом вызове какой-либо функции-метода до ее исполнения.
Для широкого применения в параллельных вычислениях мониторы должны включать элементы синхронизации. Предположим, что процесс, находясь в мониторе, должен быть приостановлен до выполнения некоторого условия. При этом требуется некий механизм, который не только приостанавливает процесс, но и освобождает монитор для других процессов. Позже, когда условие окажется выполненным, а монитор доступным, приостановленный процесс сможет продолжить свою работу с того места, где он был приостановлен.
Монитор поддерживает синхронизацию при помощи переменных условия, располагающихся (и доступных) только в мониторе. Работать с этими переменными могут две функции.
cwait (c) – приостанавливает выполнение процесса по условию с.
csignal(c) – возобновляет выполнение некоторого процесса, приостановленного вызовом cwait (c) с тем же условием. Если имеется несколько таких процессов, выбирается один из них. Если таких процессов нет, функция не делает ничего.
Обратите внимание на то, что операции wait/ signal монитора отличаются от соответствующих операций семафора. Если процесс в мониторе передает сигнал, но при этом нет ни одного ожидающего его процесса, то сигнал теряется.
Если один из процессов вошел в монитор, другие процессы, которые пытаются войти в монитор, присоединяются к очереди процессов, приостановленных в ожидании доступности монитора. Если же процесс в мониторе временно останавливается, выполнив вызов cwait (х), то процесс помещается в очередь процессов, ожидающих повторного входа в монитор при выполнении условия.
Если процесс, выполняющийся в мониторе, обнаружит изменение переменной условия х, он выполняет операцию csignal(х), которая сообщает об обнаруженном изменении соответствующей очереди.
В качестве примера использования монитора вернемся к задаче “производитель-потребитель” с ограниченным буфером. В листинге 8.4 показано решение этой задачи с использованием монитора. Модуль монитора ProducerConsumer управляет буфером для хранения и получения символов. Монитор включает две переменные условий: notfull истинно, если в буфере имеется место хотя бы для одного символа, а notempty истинно, если в буфере имеется хотя бы один символ.
Листинг 8.4. Решение задачи “производитель-потребитель” с ограниченным буфером с использованием монитора Хоара
Monitor ProducerConsumer;
Char buffer [N] /* Место для N элементов */
Int nextin, nextout; /* Указатели буфера */
Int count; /* Количество элементов в буфере */
Int notfull, notempty; /* Синхронизация */
Void Append (char x)
{
if (count == N)
cwait (notfull); /* Буфер заполнен */
Buffer [nextin] = x;
Nextin = (nextin+1) % N;
Count++; /* Добавляем элемент в буфер */
Csignal (notempty); /* Возобновление работы потребителя */
}
Void Take (char x)
{
if (count == 0)
cwait (notempty); /* Буфер пуст */
x = Buffer [nextout];
Nextout = (nextout+1) % N;
Count--; /* Удаляем элемент из буфер */
Csignal (notfull); /* Возобновляем работу производителей */
}
{ /* Тело монитора */
nextin = 0; /* Изначально буфер пуст */
nextout = 0;
cont = 0;
}
Void Producer ()
{
char x;
While (true)
{
Produce (x);
Append (x);
}
}
Void Consumer ()
{
char x;
While (true)
{
Take (x);
Consume (x);
}
}
Void Main ()
{
Perbegin (Producer, Consumer);
}
Производитель может добавлять символы в буфер только из монитора при помощи процедуры Append; прямого доступа к буферу у него нет. Сначала процедура проверяет условие notfull, чтобы выяснить, имеется ли в буфере пустое место. Если его нет, процесс приостанавливается, и в монитор может войти другой процесс (производитель или потребитель). Позже, когда в буфере появится свободное место, приостановленный процесс извлекается из очереди и возобновляет свою работу. После того как процесс поместит символ в буфер, он сигнализирует о выполнении условия notempty, что разблокирует процесс потребителя, если он был приостановлен.
Этот пример иллюстрирует разделение ответственности при работе с монитором и при использовании семафора. Монитор автоматически обеспечивает взаимоисключение: одновременное обращение производителя и потребителя к буферу невозможно. Однако программист должен корректно разместить внутри монитора примитивы Cwait и Csignal, для того чтобы предотвратить размещение элемента в заполненном буфере, или выборку из пустого буфера. В случае использования семафоров ответственность как за синхронизацию, так и за взаимоисключения полностью лежит на программисте.
Обратите внимание, что в листинге 9.5.1 процесс покидает монитор немедленно после выполнения функции Csignal. Если вызов Csignal осуществляется не в конце процедуры, то, по предложению Хоара, вызвавший эту функцию процесс приостанавливается, для того чтобы освободить монитор для другого процесса, помещается в очередь приостановленных процессов и остается там до тех пор, пока монитор вновь не освободиться.
Если выполнения условия х не ожидает ни один процесс, вызов Csignal(х) не выполняет никаких действий.
Как при работе с семафорами, так и при работе с мониторами легко допустить ошибку в функции синхронизации. Например, если опустить любой из вызовов Csignal в мониторе, то процесс, попавший в соответствующую очередь, останется там навсегда. Преимущество мониторов по сравнению с семафорами в том, что все синхронизирующие функции заключены в мониторе. Таким образом, проверить корректность синхронизации и отловить возможные ошибки оказывается проще, чем при использовании семафоров. Кроме того, при правильно разработанном мониторе доступ к защищенным ресурсам корректен независимо от запрашиваемого процесса; при использовании же семафоров доступ к ресурсу корректен только в том случае, если правильно разработаны все процессы, обращающиеся к ресурсу.