Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
У. Столлингс ГЛАВА 5 Многопроцессорные вычислен...doc
Скачиваний:
1
Добавлен:
01.03.2025
Размер:
2.98 Mб
Скачать

5.4. Семафоры

Теперь мы вернемся к механизмам операционных систем и языков про­граммирования, обеспечивающим параллельные вычисления. Этот раздел мы начнем с рассмотрения семафоров; следующие разделы будут посвящены мони­торам и передаче сообщений.

Первой большой работой, посвященной вопросам параллельных вычис­лений, стала монография Дейкстры [DIJK65], который рассматривал разра­ботку операционной системы как построение множества сотрудничающих по­следовательных процессов и создание эффективных и надежных механизмов поддержки этого сотрудничества. Эти же механизмы легко применяются и пользовательскими процессами — если процессор и операционная система делают их общедоступными.

Фундаментальный принцип заключается в том, что два или большее количест­во процессов могут сотрудничать посредством простых сигналов, так что в опреде­ленном месте процесс может приостановить работу до тех пор, пока не дождется со­ответствующего сигнала. Требования кооперации любой степени сложности могут быть удовлетворены соответствующей структурой сигналов. Для сигнализации ис­пользуются специальные переменные, называющиеся семафорами. Для передачи сигнала через семафор s процесс выполняет примитив signal(s), а для получения сигнала— примитив wait(s). В последнем случае процесс приостанавливается до тех пор, пока не осуществится передача соответствующего сигнала.

Для достижения желаемого эффекта мы можем рассматривать семафор как переменную, имеющую целое значение, над которой определены три операции.

Семафор может быть инициализирован неотрицательным значением.

  1. Операция wait уменьшает значение семафора. Если это значение становит­ся отрицательным, процесс, выполняющий операцию wait, блокируется.

  2. Операция 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);

}