- •Лекция 7: Мониторы и условные переменные
- •Монитор и переменные условия
- •Сравнение семафоров и мониторов
- •Поддержка синхронизации в языках программирования
- •Передача сообщений
- •Взаимодействие процессов на разных машинах
- •Всегда ли параллельные процессы дают выигрыш?
- •Мораль: нити очень удобны, но не бесплатны
Лекция 7: Мониторы и условные переменные
Монитор и условные переменные
Задача о производителе и потребителе
Монитор и переменные условия
Семафоры – это большой шаг вперед в области синхронизации. Однако, их использование для взаимного исключения и для ожидания не всегда удобно, делает код трудно читаемым.
Мониторы предложены с целью разделить эти задачи и упростить программирование. Монитор содержит общие переменные, разделяемые процессами и функции доступа к общим переменным. Тело функции монитора является критической секцией и должно быть защищено от повторного входа. Другими словами, только один процесс может выполнять функцию монитора.
Первоначально (Хоар, Бринч Хансен) определяли монитор как конструкцию в языке программирования, при этом lock/unlock подставлялись в ходе компиляции соответственно в начале и на выходе из функций монитора. Во многих ОСOS/2, Windows NT, Solaris мониторы строятся на основе явного вызова примитивов взаимного.
Рассмотрим пример взаимодействия двух процессов – передача (прием) данных через кольцевой буфер.
class Buffer {
int buf[N];
int head, tail
public:
Buffer() { head = tail = 0; }
void Put(int value);
void Get(int& value);
};
void Buffer::Put(int value)
{
while( (tail+1)%N == head); // пока буфер полон - ждать
// или while( (tail+1)%N == head) Sleep(1);
buf[tail] = value; // Разместить данные в буфере
tail = (tail+1)%N;
}
void Buffer::Get(int& value)
{
while( tail == head); // пока буфер пуст - ждать
// или while( tail == head) Sleep(1);
value = buf[head]; // Взять данные из буфера
head = (head +1)%N;
}
Приведенное решение справедливо только для двух процессов: один передает данные, а другой принимает. Процессы используют разные функции и модифицируют разные переменные: один – head, а другой –tail.
Если допускается существование нескольких передающих или нескольких читающих процессов, то операции чтения и записи должны быть защищены критической секцией. В определение класса необходимо добавить переменную MUTEX lock, а в начало и в конец функцийPut иGet добавить соответственно захват (lock.Acquire()) и освобождение (lock.Release()) критической секции.
Однако, это решение не верно. Проблема в том, что ожидание внутри критической секции в данном случае приводит к блокировке. Например, если буфер пуст, то мы ждем в цикле while внутри критической секции и другой процесс не сможет выполнить функциюPut, поскольку критическая секция занята.
Для реализации ожидания в мониторах определены специальные переменные (переменныя условия – condition variables), с каждой связана очередь нитей ожидающих событие вне критической секции.
Для переменных условия определены функции: wait(suspend), signal(resume), broadcast.
wait выход из критической секции, ожидание, возврат в критическую секцию
signal будит одного ожидающего, если кто-то ждет
broadcast будит всех ожидающих, если кто-то ждет
Получаем следующее решение:
class Buffer {
MUTEX lock
CONDITION full,empty;
int buf[N];
int head, tail
public:
Buffer() { head = tail = 0; }
void Put(int value);
void Get(int& value);
};
void Buffer::Put(int value)
{
lock.Acquire();
while( (tail+1)%N == head) full.wait(lock); // ждать
buf[tail] = value; // Разместить данные в буфере
tail = (tail+1)%N;
empty.signal();
lock.Release();
}
void Buffer::Get(int& value)
{
lock.Acquire();
while( tail == head) empty.wait(lock); // ждать
value = buf[head]; // Взять данные из буфера
head = (head +1)%N;
full.signal();
lock.Release();
}
Два стиля мониторов: Mesa vs. Hoar
Различие в том, как осуществляется возврат в критическую секцию после wait.
MESA (реально применяется): послеsignal не происходит принудительного переключения контекста, разбуженный просто ставится в очередь готовых и должен вновь проверять право на вход в монитор!
HOAR (описан в учебниках): послеsignal происходит принудительное переключения контекста, управление получает разбуженный процесс и только в момент его выхода из монитора процесс, выполнившийsignal, получает возможность продолжить выполнение.
Рассмотренный пример справедлив для обоих стилей. Если заменить while наif, то решение останется справедливым для монитора Хоара, но не будет работать для монитора стиляMESA.
wait иsignal не эквивалентны семафорным примитивам - нет счетчика, поэтомуsignal, выполненный раньшеwait эквивалентен пустому оператору.