- •Глава 5
- •5.1. Принципы параллельных вычислений
- •Простой пример
- •Взаимодействие процессов
- •Листинг 5.1. Взаимные исключения
- •5.2. Взаимоисключения
- •Листинг 5.3. Алгоритм Петерсона для двух процессов
- •5.3. Взаимоисключения: аппаратная поддержка
- •Специальные машинные команды
- •5.4. Семафоры
- •5.5. Мониторы
- •5.6. Передача сообщений
- •Синхронизация
- •Принцип работы очереди
- •5.7. Задача читателей/писателей
- •Приоритетное чтение
- •5.8. Резюме, ключевые термины и контрольные вопросы
- •5.9. Pекомендуемая литература
- •5.10. Задачи
5.4. Семафоры
Теперь мы вернемся к механизмам операционных систем и языков программирования, обеспечивающим параллельные вычисления. Этот раздел мы начнем с рассмотрения семафоров; следующие разделы будут посвящены мониторам и передаче сообщений.
Первой большой работой, посвященной вопросам параллельных вычислений, стала монография Дейкстры [DIJK65], который рассматривал разработку операционной системы как построение множества сотрудничающих последовательных процессов и создание эффективных и надежных механизмов поддержки этого сотрудничества. Эти же механизмы легко применяются и пользовательскими процессами — если процессор и операционная система делают их общедоступными.
Фундаментальный принцип заключается в том, что два или большее количество процессов могут сотрудничать посредством простых сигналов, так что в определенном месте процесс может приостановить работу до тех пор, пока не дождется соответствующего сигнала. Требования кооперации любой степени сложности могут быть удовлетворены соответствующей структурой сигналов. Для сигнализации используются специальные переменные, называющиеся семафорами. Для передачи сигнала через семафор s процесс выполняет примитив signal(s), а для получения сигнала— примитив wait(s). В последнем случае процесс приостанавливается до тех пор, пока не осуществится передача соответствующего сигнала.
Для достижения желаемого эффекта мы можем рассматривать семафор как переменную, имеющую целое значение, над которой определены три операции.
Семафор может быть инициализирован неотрицательным значением.
Операция wait уменьшает значение семафора. Если это значение становится отрицательным, процесс, выполняющий операцию wait, блокируется.
Операция signal увеличивает значение семафора. Если это значение не положительно, то заблокированный операцией wait процесс деблокируется.
Не имеется никаких иных способов получения информации о значении семафора или изменения его значения, кроме перечисленных.
В листинге 5.5 приведено более формальное определение примитивов семафоров. Предполагается, что примитивы wait и signal атомарны, т.е. они не могут быть прерваны, и каждая из подпрограмм может рассматриваться как единый шаг. Более ограниченная версия семафора, известная как бинарный семафор, представлена в листинге 5.6. Бинарный семафор может принимать только значения 0 или 1. В принципе реализация бинарного семафора должна быть более простой задачей; можно также показать, что все задачи, решаемые с применением обычных семафоров, могут быть решены и с использованием лишь бинарных семафоров (см. задачу 5.13).
Листинг 5.5. Определение семафорных примитивов
struct semaphore
{
int count;
queueType queue;
}
void wait(semaphore s)
{
s.count--;
if (s.count < 0)
{
Поместить процесс в s.queue
Заблокировать процесс
}
}
void signal(semaphore s)
{
s. count++;
if (s.count <= 0)
{
Удалить процесс Р из S.queue
Поместить процесс Р в список активных
}
Листинг 5.6. Определение примитивов бинарного семафора
struct binary_semaphore
{
enum { zero, one } value;
queueType queue;
}
void waitB(binary_semaphore s)
{
if (s.value == 1)
s.value = 0; else
{
Поместить процесс в s.queue Заблокировать процесс
}
}
void signalB(binary_semaphore s)
{
if (s.queue.is_empty())
s.value = 1;
else
{
Удалить процесс Р из S.queue
Поместить процесс Р в список активных
}
}
Для хранения процессов, ожидающих как обычные, так и бинарные семафоры, используется очередь. При этом возникает вопрос о порядке извлечения процессов из данной очереди. Наиболее корректный способ — использование принципа "первым вошел — первым вышел" (first-in-first-out — FIFO). При этом первым из очереди освобождается процесс, который был заблокирован дольше других. Семафор, использующий данный метод, называется сильным семафором (strong semaphore). Семафор, порядок извлечения процессов из очереди которого не определен, называется слабым семафором (weak semaphore). На рис. 5.2 (из [DENN84]) приведен пример работы сильного семафора. Здесь процессы А, В и С зависят от результатов работы процесса D. Изначально работает процесс А (1); процессы В, С и D находятся в списке активных процессов, ожидая своей очереди. Значение семафора равно 1, это указывает на то, что один из результатов работы процесса D имеется в наличии. Когда процесс А выполняет инструкцию wait, он тут же получает разрешение на дальнейшую работу и вновь становится в очередь на выполнение в списке активных процессов. Затем приступает к работе процесс В (2), который в конечном счете также выполняет инструкцию wait, в результате чего процесс приостанавливается, давая возможность приступить к работе процессу D (3). Когда процесс D завершает работу над получением нового результата, он выполняет инструкцию signal, которая позволяет процессу В перейти из списка приостановленных процессов в список активных (4). Процесс D присоединяется к списку активных процессов, и к выполнению приступает процесс С (5), но тут же приостанавливается при выполнении инструкции wait. Точно так же приостанавливается и выполнение процессов А и В, давая возможность процессу D приступить к работе (6). После того как получается новый результат процесса D, им выполняется инструкция signal, которая и переводит процесс С из списка приостановленных в список активных. Последующие циклы выполнения процесса D переведут в список активных процессы А и В.
В следующем подразделе рассматривается алгоритм взаимоисключений (листинг 5.7), использование сильного семафора в котором гарантирует невозможность голодания, но слабый семафор такой гарантии не дает. Далее мы будем считать, что работают сильные семафоры, поскольку они более удобны и обычно именно этот вид семафоров используется операционной системой.
Взаимные исключения
В листинге 5.7 показано простое решение задачи взаимоисключений с использованием семафора s (сравните с листингом 5.1). Пусть у нас имеется п процессов, идентифицируемых массивом P(i). В каждом из процессов перед входом в критический раздел выполняется вызов wait(s). Если значение s становится отрицательным, процесс приостанавливается. Если же значение равно 1, оно уменьшается до нуля и процесс немедленно входит в критический раздел; поскольку s больше не является положительным, ни один другой процесс не может войти в критический раздел.
Листинг 5.7. Взаимоисключения с использованием семафоров
/* Программа взаимного исключения */
const int n = /* Количество процессов */;
semaphore s = 1;
void P(int i)
{
while(true)
{
wait(s);
/* Критический раздел */;
signal(s);
/* Остальной код */;
}
}
void main(}
{
parbegin(P(l), P(2) , . . ., P(n) ) ;
} __
Семафор инициализируется значением 1. Следовательно, первый процесс, выполняющий инструкцию wait, сможет немедленно попасть в критический раздел, устанавливая при этом значение семафора равным 0. Любой другой процесс при попытке войти в критический раздел обнаружит, что он занят. Соответственно, произойдет блокировка процесса, а значение семафора будет уменьшено до -1. Пытаться войти в критический раздел может любое количество процессов; каждая неуспешная попытка уменьшает значение семафора. После того как процесс, вошедший в критический раздел первым, покидает его, s увеличивается, и один из заблокированных процессов (если таковые имеются) удаляется из связанной с семафором очереди и активизируется. Таким образом, как только планировщик операционной системы предоставит ему возможность выполнения, процесс тут же сможет войти в критический раздел.
На рис. 5.3 показана возможная последовательность действий трех процессов при использовании технологии взаимоисключений, представленной в листинге 5.7. В этом примере три процесса (А, В, С) обращаются к разделяемому ресурсу, защищенному семафором lock. Процесс А выполняет wait (lock); поскольку в этот момент семафор имеет значение 1, процесс А может немедленно войти в критический раздел и значение семафора становится равным 0. Пока А находится в критическом разделе, и В, и С выполняют операцию wait, после чего в заблокированном состоянии ожидают доступности критического раздела. Когда процесс А покидает критический раздел и выполняет операцию signal (lock), процесс В (являющийся первым в очереди) получает возможность войти в критический раздел.
Программа, приведенная в листинге 5.7, может так же хорошо работать и в том случае, когда одновременно в критическом разделе находятся несколько процессов. Для этого достаточно инициализировать семафор соответствующим значением. Следовательно, в любой момент времени значение s. count интерпретируется следующим образом.
• s.count > 0: значение s.count определяет количество процессов, которые могут выполнить wait(s) без приостановки процесса (подразумевается, что промежуточные вызовы signal (s) отсутствуют).\
•s.count < 0: абсолютное значение s.count определяет количество приостановленных процессов в очереди s.queue.
Задача производителя/потребителя
Сейчас мы рассмотрим одну из общих задач параллельных вычислений — задачу производителя/потребителя. Вот ее обобщенная формулировка. Имеются один или несколько производителей, генерирующих данные некоторого типа (записи, символы и т.п.) и помещающих их в буфер, а также единственный потребитель, который извлекает помещенные в буфер элементы по одному. Требуется защитить систему от перекрытия операций с буфером, т.е. обеспечить, чтобы одновременно получить доступ к буферу мог только один процесс (производитель или потребитель). Мы рассмотрим несколько решений этой задачи, с тем чтобы проиллюстрировать как мощь семафоров, так и встречающиеся при их использовании ловушки.
Для начала предположим, что буфер бесконечен и представляет собой линейный массив элементов. Говоря абстрактно, мы можем определить функции производителя и потребителя следующим образом:
/* Производитель */ /* Потребитель */
while(true) while(true)
{ {
/* Производство while (in <= out)
элемента v */ /* Бездействие */;
b[in] = v; w = b[out];
in++; out++;
} /* Потребление w*/
}
На рис. 5.4 показана структура буфера b. Производитель может генерировать данные и сохранять их в буфере со своей индивидуальной скоростью. Всякий раз при сохранении увеличивается индекс in. Потребитель поступает аналогично, с тем отличием, что он не должен считывать данные из пустого буфера. Следовательно, перед выполнением считывания он должен убедиться, что производитель обогнал его (in > out).
Попытаемся реализовать нашу систему с использованием бинарных семафоров. В листинге 5.8 приведена первая попытка реализации. Вместо работы с индексами in и out мы можем просто отслеживать количество элементов в буфере посредством целой переменной n = in - out. Для осуществления взаимного исключения используется семафор s; семафор delay применяется для ожидания потребителя при пустом буфере.
Листинг 5.8. [Неверное] решение задачи производитель/потребитель с использованием бинарных семафоров
int n;
binary_semaphore s = 1;
binary_semaphore delay = 0;
void producer()
{
while(true)
{
produce();
waitB (s);
append();
n++;
if (n == 1) signalB(delay);
signalB(s);
}
}
void consumer()
{
waitB(delay);
while(true)
{
waitB(s);
take();
n--;
signalB(s) ;
consume();
if (n == 0) waitB(delay);
}
}
void main()
{
n = 0;
parbegin(producer,consumer); }
Решение представляется достаточно простым и очевидным. Производитель может добавлять данные в буфер в любой момент времени. Перед добавлением он выполняет waitB (s), а после добавления— signalB (s), чтобы предотвратить обращение к буферу других производителей или потребителя на все время операции добавления данных в буфер. Кроме того, при работе в критическом разделе производитель увеличивает значение п. Если n = 1, то перед этим добавлением данных в буфер он был пуст, так что производитель выполняет signalB (delay), чтобы сообщить об этом потребителю. Потребитель начинает с ожидания производства первого элемента, используя вызов waitB (delay). Затем потребитель получает данные из буфера и уменьшает значение n в своем критическом разделе. Если производители опережают потребителя (достаточно распространенная ситуация), то потребитель будет редко блокирован семафором delay, поскольку n обычно положительно. Следовательно, благополучно работают и производитель, и потребитель.
Тем не менее в предложенной программе имеется один изъян. Когда потребитель исчерпывает буфер, он должен сбросить семафор delay с помощью инструкции if (n == 0) waitB (delay);, чтобы дождаться размещения данных в буфере производителем. Рассмотрим сценарий, приведенный в табл. 5.2. В строке 14 потребитель не выполняет операцию waitB. Он действительно исчерпал буфер и установил n равным 0 в строке 8, но до проверки значения n в строке 14 оно было изменено производителем. В результате signals не соответствует предшествующему waitB. Значение n, равное -1 в строке 20, означает, что потребитель пытается извлечь из буфера несуществующий элемент. Простое перемещение проверки в критический раздел потребителя недопустимо, так как может привести к взаимоблокировке (например, после строки 8).
Решение проблемы заключается во введении вспомогательной переменной, значение которой присваивается в критическом разделе и используется вне его, как показано в листинге 5.9. Внимательно рассмотрев приведенный код, вы убедитесь в отсутствии возможных взаимоблокировок.
Листинг 5.9. Верное решение задачи производитель/потребитель с использованием бинарных семафоров
int n;
binary_semaphore s = 1;
binary_semaphore delay = 0;
void producer()
{
while(true)
{
produce(};
waitB(s) ;
append();
n++;
if (n == 1) signalB(delay);
signals(s);
}
}
void consumer()
{
int m; /* Локальная переменная */
waitB(delay);
while(true)
{
waitB (s);
take ();
n--;
m = n ;
signalB(s);
consume();
if (m == 0) waitB(delay);
}
}
void main()
{
n = 0;
parbegin(producer,consumer); }
Несколько более простое решение (приведенное в листинге 5.10) можно получить при использовании обобщенных семафоров (именуемых также семафорами со счетчиками). Переменная n в этом случае является семафором; ее значение остается равным количеству элементов в буфере. Предположим теперь, что при переписывании этой программы произошла ошибка, и операции signal (s) и signal (n) оказались взаимозамененными. Это может привести к тому, что операция signal (n) будет выполняться в критическом разделе производителя без прерывания потребителя или другого производителя. Повлияет ли это на выполнение программы? Нет, поскольку потребитель в любом случае должен ожидать установки обоих семафоров перед продолжением работы.
Листинг 5.10. Решение задачи производитель/потребитель с использованием семафоров
semaphore n = 0;
semaphore s = 1;
void producer ()
{
while(true)
{
produce ();
wait (s);
append(); signal(s);
signal(n);
}
}
void consumer()
{
while(true)
{
wait(n);
wait (s); t
take ();
signal(s);
consume();
}
}
void main()
{
parbegin(producer, consumer);
}
Теперь предположим, что взаимозаменены операции wait(n) и wait(s). Это приведет к фатальным последствиям. Если пользователь войдет в критический раздел, когда буфер пуст (n.count = 0), то ни один производитель не сможет добавить данные в буфер и система окажется в состоянии взаимной блокировки. Это хороший пример тонкости работы с семафорами и сложности корректной разработки параллельно работающих процессов.
А теперь добавим к нашей задаче новое, достаточно реалистичное ограничение — конечность буфера. Буфер рассматривается нами как циклическое хранилище (см. рис. 5.5), при работе с которым значения указателей должны выражаться по модулю размера буфера. При этом выполняются следующие условия.
Блокировка: Деблокирование: Производитель: вставка в полный буфер Потребитель: удаление элемента из буфера Потребитель: удаление из пустого буфера Производитель: вставка элемента в буфер |
Функции производителя и потребителя при этом могут быть записаны следующим образом (переменные in и out инициализированы значением 0):
Производитель:
while(true)
{
/* Производство элемента v*/;
while(in+1)%n == out)
/* Бездействие */;
b[in] = v;
in = (in+1) % n;
}
Потребитель:
while(true)
{
while(in == out)
/* Бездействие */;
w = b[out];
out = (out+1) % n;
/* Потребление
элемента w */;
}
В листинге 5.11 приведено решение с использованием обобщенных семафоров. Для отслеживания пустого места в буфере в программу добавлен семафор е.
Листинг 5.11. Решение задачи производитель/потребитель с ограниченным буфером
const int sizeofbuffer = /* Размер буфера */;
semaphore s = 1;
semaphore n = 0;
semaphore e = sizeofbuffer;
void producer()
{
while(true)
{
produce();
wait(e);
wait(s);
append();
signal(s);
signal(n) ;
}
}
void consumer!)
{
while(true)
{
wait(n) ;
wait (s);
take ();
signal(s) ;
signal(e);
consume();
}
}
void main()
{
parbegin(producer, consumer);
}
Реализация семафоров
Как упоминалось ранее, главное условие корректности работы семафоров заключается в требовании атомарности операций wait и signal. Один из очевидных путей выполнения этого условия состоит в реализации семафоров в аппаратном или микропрограммном обеспечении. Если этот путь недоступен, применяются различные программные подходы. Суть проблемы заключается в реализации взаимных исключений: в определенный момент времени работать с семафором посредством операций wait или signal может только один процесс. Следовательно, подойдет любая из рассматривавшихся программных схем, такая, как алгоритмы Деккера или Петерсона; но это может привести к определенным накладным расходам. Можно также использовать одну из схем поддержки взаимоисключений на аппаратном уровне. Так, в листинге 5.12,а показано, как можно использовать инструкцию проверки и установки значения. В этой реализации, как и в листинге 5.7, семафор представляет собой структуру; однако теперь он включает новый целый компонент s . flag. Конечно, при таком способе реализации семафоров неизбежно пережидание занятости, но поскольку операции wait и signal относительно небольшие, время ожидания минимально.
В однопроцессорной системе можно воспользоваться запретом прерываний на время выполнения операций wait и signal, как предложено в листинге 5.12,6. Повторимся еще раз — малое время ожидания занятости этих операций означает целесообразность применения предложенного подхода.
Листинг 5.12. Две возможные реализации семафоров
/* а.) Инструкция проверки и установки */
wait (s)
{
while(testset(s.flag))
/* Бездействие */;
s.count--;
if (s.count < 0)
{
Поместить этот процесс в s.queue Заблокировать процесс и установить s.flag равным 0
}
else
s.flag = 0;
}
signal(s)
{
while(testset(s.flag)}
/* Бездействие */;
s.count++;
if (s.count <= 0)
{
Удалить процесс Р из s.queue Поместить Р в список активных процессов
}
s.flag = 0;
}
/* б) Запрет прерываний */
wait(s)
{
Запретить прерывания;
s.count--;
if (s.count < 0)
{
Поместить этот процесс в s.queue Заблокировать процесс
}
else
Разрешить прерывания;
}
signal(s)
{
Запретить прерывания;
s.count++;
if (s.count <= 0)
{
Удалить процесс Р из s.queue Поместить Р в список активных процессов
}
Разрешить прерывания;
}
Задача о парикмахерской
В качестве другого примера использования семафоров для реализации параллельных вычислений рассмотрим простую задачу о парикмахерской.3 Этот пример весьма поучителен, так как задача, возникающая при попытке обеспечить простой доступ в парикмахерскую, сродни тем, которые возникают в реальных операционных системах.
В нашей парикмахерской три кресла, три парикмахера, зал ожидания, в котором четыре клиента могут разместиться на диване, а остальные — стоя (см. рис. 5.6). Правила пожарной безопасности ограничивают общее количество клиентов внутри помещения 20 людьми. В нашем примере мы предполагаем, что всего мастерская должна обслужить 50 клиентов.
Клиент не может войти в парикмахерскую, если она полностью заполнена другими клиентами. Оказавшись внутри, клиент либо присаживается на диван, либо стоит, если последний занят. Когда парикмахер освобождается, к нему отправляется наиболее долго ожидающий клиент с дивана; если имеются стоящие клиенты, то тот из них, кто ожидает дольше других, присаживается на диван. По окончании стрижки принять плату может любой парикмахер, но так как кассир в парикмахерской лишь один, плата принимается в один момент времени только от одного клиента. Рабочее время парикмахера разделяется на стрижку, принятие оплаты от клиента и сон в своем кресле в ожидании очередного клиента.
Неполное решение задачи о парикмахерской
В листинге 5.13 показана реализация парикмахерской с использованием семафоров; предполагается, что все очереди семафоров обрабатываются по принципу "первым вошел — первым вышел". В листинге для экономии места функции расположены в два столбца.
Основное тело программы активизирует процессы 50 клиентов, трех парикмахеров и одного кассира. Рассмотрим теперь назначение различных синхронизирующих операторов.
• Вместимость парикмахерской и дивана. Вместимость парикмахерской и дивана управляется семафорами max_capacity и sofa, соответственно. Каждый раз при попытке клиента войти в парикмахерскую значение семафора max_capacity уменьшается на 1; когда клиент покидает парикмахерскую, оно увеличивается. Если парикмахерская заполнена, то процесс клиента приостанавливается функцией wait (max_capacity). Аналогично обрабатывается и попытка присесть на диван.
Емкость парикмахерских кресел. В наличии имеются три парикмахерских кресла, и следует обеспечить их корректное использование. Семафорbarber_chair гарантирует одновременное обслуживание не более трех клиентов, так чтобы один клиент не оказался на коленях у другого. Клиент не поднимется с дивана до тех пор, пока не окажется свободным хотя бы одно кресло (вызов wait (barber_chair)), а каждый парикмахер сообщает о том, что его кресло освободилось (вызов signal (barber_chair)). Справедливый доступ к парикмахерским креслам гарантируется организацией очередей семафоров: клиент, который первым блокирован в очереди, первым же и приглашается на стрижку. Заметим, что если в процедуре клиента вызов wait (barber_chair) разместить после signal (sofa), то каждый клиент будет только присаживаться на диван, после чего немедленно вскакивать и занимать стартовую позицию у кресла, создавая излишнюю толкотню и мешая работать парикмахеру.
Размещение клиента в кресле. Семафор cust_ready обеспечивает подъем спящего парикмахера, сообщая ему о новом клиенте. Без этого семафора парикмахер никогда не отдыхал бы и приступал к стрижке немедленно после того, как очередной клиент покинет кресло. При отсутствии клиента в этот момент парикмахер стриг бы воздух.
Удержание клиента в кресле. Если уж клиент оказался в кресле, он должен отсидеть там до окончания стрижки, о чем просигнализирует семафор
finished.
Ограничение количества клиентов в креслах. Семафор barber_chair предназначен для ограничения количества клиентов в креслах — их не должно быть более трех. Однако одного семафора barber_chair для этого недостаточно. Клиент, который не получит процессорное время непосредственно после того, как парикмахер сообщит о завершении работы над его прической (вызовsignal (finished)), останется в кресле (например, впав в транс или глубоко задумавшись о задаче о парикмахерской), в то время как в этом же кресле будет стараться устроиться новый клиент. Для решения данной задачи используется семафор leave_b_chair, который не позволяет парикмахеру пригласить нового клиента до тех пор, пока предыдущий не покинет кресло.
Оплата стрижки. Естественно, с деньгами надо быть особенно осторожным. Кассир должен быть уверен, что каждый клиент, покидая парикмахерскую, сперва расплатится, а каждый клиент, оплатив стрижку, должен получить чек. Это достигается передачей денег кассиру из рук в руки — каждый клиент, покинув кресло, оплачивает услуги парикмахера, после чего дает знать об этом кассиру (вызов signal (payment)) и дожидается получения кассового чека (вызов wait (receipt)). Кассир осуществляет прием платежей, ожидая сигнала о платеже, принимая деньги, а затем сообщая об этом. Здесь следует постараться избежать ряда программных ошибок. Если вызов signal (payment) выполняется непосредственно перед вызовом pay (), то клиент может оказаться прерванным в этот момент, и кассир будет пытаться принять не переданные деньга. Еще более серьезная ошибка происходит при обмене строк
signal (payment) и wait (receipt). Это может привести к взаимоблокировке всех клиентов и кассира их операциями wait.
• Координация действий кассира и парикмахера. В целях экономии средств парикмахерская не нанимает отдельного кассира. Это действие выполняет парикмахер, когда не стрижет клиента. Для того чтобы обеспечить выполнение парикмахером в один момент времени только одной функции, используется семафор coord.
Назначение каждого семафора программы указано в табл. 5.3.
Листинг 5.13. Неполное решение задачи о парикмахерской
semaphore max_capacity = 20;
semaphore sofa = 4;
semaphore barber_chair = 3;
semaphore coord = 3;
semaphore cust_ready = 0, finished = 0,
leave_b_chair = 0, payment = 0,
receipt = 0;
void customer() void barber()
{ {
wait(max_capacity); while(true)
enter_shop (); {
wait(sofa); wait(cust_ready);
sit_on_sofa(); wait(coord);
wait(barber_chair); cut_hair();
get_up_from_sofa(}; ' signal(coord);
signal(sofa); signal(finished);
sit_in_barber_chair(); wait(leave_b_chair);
signal(cut_ready); signal(barber_chair);
wait(finished); }
leave_barber_chair(); }
signal(leave_b_chair); void cashier()
pay () ; {
signal(payment); while(true)
wait(receipt); {
exit_shop(); wait(payment);
signal(max_capacity); wait(coord);
} accept_pay();
signal(coord); signal(receipt);
}
}
void main()
{
parbegin(customer,...[50 раз]...,customer,
barber,barber,barber,cashier); }
Процесс кассира можно устранить, внеся функцию оплаты в процедуру парикмахера. Каждый парикмахер будет последовательно стричь и принимать плату. Однако при наличии одного кассового аппарата необходимо ограничить доступ к функции accept_pay() одним парикмахером в каждый момент времени. Этого можно добиться, рассматривая функцию как критический раздел и оградив ее соответствующим семафором.
Полное решение задачи о парикмахерской
Несмотря на приложенные усилия (см. листинг 5.13), у нас остались определенные сложности. Решение одной из проблем содержится в оставшейся части подраздела, а решение остальных останется читателю в качестве упражнений (см. задачу 5.19).
В листинге 5.13 отражена проблема, которая может привести к некорректной работе с клиентами. Предположим, что в настоящий момент в парикмахерских креслах сидят три клиента. Они, скорее всего, заблокированы вызовами wait (finished) и в соответствии с организацией очереди будут деблокированы в том порядке, в котором они садились в кресла. Но что если один из парикмахеров очень быстро работает (или один из клиентов лысый)? Получится ситуация, когда одного клиента будут вытаскивать из кресла и заставлять платить за незаконченную прическу, в то время как другого, полностью постриженного клиента будут держать в кресле силой.
Эта проблема решается добавлением новых семафоров, как показано в листинге 5.14. Мы присваиваем каждому пользователю номер (как если бы при входе в парикмахерскую каждый клиент получал номерок). Семафор mutexlобеспечивает защиту доступа к глобальной переменной count, гарантируя уникальность номера каждого клиента. Семафор finished превратился в массив из 50 семафоров. Когда клиент садится в кресло, он выполняет инструкцию wait (finished[custnr] ), ожидая свой собственный семафор. По окончании стрижки парикмахер выполняет инструкцию signal (finished [b_cust] ), отпуская из кресла того клиента, которого он стриг.
Нам остается рассмотреть, каким образом парикмахер узнает номер клиента. Клиент помещает свой номер в очередь enqueue 1 непосредственно перед тем, как сообщить парикмахеру о готовности при помощи семафора cust_ready. Когда парикмахер готов стричь клиента, dequeue I (b_cust) удаляет номер клиента из очереди и помещает его в локальную переменную парикмахера b_cust.
Листинг 5.14. Полное решение задачи о парикмахерской
semaphore max_capacity = 20; semaphore sofa = 4;
semaphore barber_chair = 3, coord= 3;
semaphore mutexl=1, mutex2= 1;
semaphore cust_ready = 0, leave__b_chair = 0,
payment = 0, receipt = 0;
semaphore finished[50] = {0};
void customer() void barber ()
{ {
int custnr; int b_cust;
wait(max_capacity); while(true)
enter_shop(); {
wait(mutexl); wait(cust_ready);
count++; wait(mutex2);
custnr = count; dequeuel(b_cust);
signal(mutexl); signal(mutex2);
wait(sofa); wait(coord);
sit_on_sofa(); cut_hair();
wait(barber_chair); signal(coord);
get_up_from_sofa(); signal(finished[b__cust]);
signal (sofa) ; wait (leave_b_chair*) ;
sit_in_barber_chair(); signal(barber_chair);
wait (mutex2) ; }
enqueuel(custnr); }
signal(cut_ready); void cashier()
signal(mutex2); {
wait(finished[custnr]); while(true)
leave_barber_chair(); {
signal(leave_b_chair); wait(payment);
pay(); wait(coord);
Signal(payment); accept_pay();
wait(receipt); signal(coord);
exit_shop(); signal(receipt);
signal(max_capacity); }
} }
void main()
{
parbegin(customer,...[50 раз]...,customer,
barber,barber,barber,cashier);
}
