Хотя решение задачи producer-consumer с помощью семафоров выглядит
достаточно изящно, программирование с их использованием требует
повышенной осторожности и внимания, чем отчасти напоминает
программирование на языке Ассемблера. Допустим, что в рассмотренном
примере мы случайно поменяли местами операции P, сначала выполнив
операцию для семафора mutex, а уже затем для семафоров full и
empty. Допустим теперь, что потребитель, войдя в свой критический
участок (mutex сброшен), обнаруживает, что буфер пуст. Он
блокируется и начинает ждать появления сообщений. Но производитель
не может войти в критический участок для передачи информации, так
как тот заблокирован потребителем. Получаем тупиковую ситуацию.
В сложных программах произвести анализ правильности использования
семафоров с карандашом в руках становится очень непросто. В то же
время обычные способы отладки программ зачастую не дают результата,
поскольку возникновение ошибок зависит от interleaving атомарных
операций, и ошибки могут быть трудновоспроизводимы. Для того чтобы
облегчить работу программистов, в 1974 году Хором (Hoare) был
предложен механизм еще более высокого уровня, чем семафоры,
получивший название мониторов. Мы с вами рассмотрим конструкцию,
несколько отличающуюся от оригинальной.
Мониторы представляют собой тип данных, который может быть с
успехом внедрен в объектно-ориентированные языки программирования.
Монитор обладает собственными переменными, определяющими его
состояние. Значения этих переменных извне могут быть изменены
только с помощью вызова функций-методов, принадлежащих монитору. В
свою очередь, эти функции-методы могут использовать в работе только
данные, находящиеся внутри монитора, и свои параметры. На
абстрактном уровне можно описать структуру монитора следующим
образом:
monitor monitor_name {
описание внутренних переменных ;
void m1(...){...
}
void m2(...){...
}
...
void mn(...){...
}
{
блок инициализации
внутренних переменных;
}
}
Здесь функции m1,..., mn представляют собой функции-методы
монитора, а блок инициализации внутренних переменных содержит
операции, которые выполняются один и только один раз: при создании
монитора или при самом первом вызове какой-либо функции-метода до
ее исполнения.
Важной особенностью мониторов является то, что в любой момент
времени только один процесс может быть активен, т. е. находиться в
состоянии готовность или исполнение, внутри данного монитора.
Поскольку мониторы представляют собой особые конструкции языка
программирования, компилятор может отличить вызов функции,
принадлежащей монитору, от вызовов других функций и обработать его
специальным образом, добавив к нему пролог и эпилог, реализующий
взаимоисключение. Так как обязанность конструирования механизма
взаимоисключений возложена на компилятор, а не на программиста,
работа программиста при использовании мониторов существенно
упрощается, а вероятность возникновения ошибок становится
меньше.
Однако одних только взаимоисключений недостаточно для того, чтобы в
полном объеме реализовать решение задач, возникающих при
взаимодействии процессов. Нам нужны еще и средства организации
очередности процессов, подобно семафорам full и empty в предыдущем
примере. Для этого в мониторах было введено понятие условных
переменных (condition variables)1), над которыми можно совершать
две операции wait и signal, отчасти похожие на операции P и V над
семафорами.
Если функция монитора не может выполняться дальше, пока не наступит
некоторое событие, она выполняет операцию wait над какой-либо
условной переменной. При этом процесс, выполнивший операцию wait,
блокируется, становится неактивным, и другой процесс получает
возможность войти в монитор.
Когда ожидаемое событие происходит, другой процесс внутри
функции-метода совершает операцию signal над той же самой условной
переменной. Это приводит к пробуждению ранее заблокированного
процесса, и он становится активным. Если несколько процессов
дожидались операции signal для этой переменной, то активным
становится только один из них. Что можно предпринять для того,
чтобы у нас не оказалось двух процессов, разбудившего и
пробужденного, одновременно активных внутри монитора? Хор
предложил, чтобы пробужденный процесс подавлял исполнение
разбудившего процесса, пока он сам не покинет монитор. Несколько
позже Хансен (Hansen) предложил другой механизм: разбудивший
процесс покидает монитор немедленно после исполнения операции
signal. Мы будем придерживаться подхода Хансена.
Необходимо отметить, что условные переменные, в отличие от
семафоров Дейкстры, не умеют запоминать предысторию. Это означает,
что операция signal всегда должна выполняться после операции wait.
Если операция signal выполняется над условной переменной, с которой
не связано ни одного заблокированного процесса, то информация о
произошедшем событии будет утеряна. Следовательно, выполнение
операции wait всегда будет приводить к блокированию процесса.
Давайте применим концепцию мониторов к решению задачи
производитель-потребитель.
monitor ProducerConsumer {
condition full, empty;
int count;
void put() {
if(count == N) full.wait;
put_item;
count += 1;
if(count == 1) empty.signal;
}
void get() {
if (count == 0) empty.wait;
get_item();
count -= 1;
if(count == N-1) full.signal;
}
{
count = 0;
}
}
Producer:
while(1) {
produce_item;
ProducerConsumer.put();
}
Consumer:
while(1) {
ProducerConsumer.get();
consume_item;
}
Легко убедиться, что приведенный пример действительно решает
поставленную задачу.
Реализация мониторов требует разработки специальных языков
программирования и компиляторов для них. Мониторы встречаются в
таких языках, как параллельный Евклид, параллельный Паскаль, Java и
т. д. Эмуляция мониторов с помощью системных вызовов для обычных
широко используемых языков программирования не так проста, как
эмуляция семафоров. Поэтому можно пользоваться еще одним механизмом
со скрытыми взаимоисключениями, механизмом, о котором мы уже
упоминали, – передачей сообщений.
Мониторы
85
0
3 минуты
Темы:
Понравилась работу? Лайкни ее и оставь свой комментарий!
Для автора это очень важно, это стимулирует его на новое творчество!