Разработка модели межпроцессного взаимодействия на примере проблемы «Производитель – потребитель» с использованием семафоров Проблема производитель-потребитель является одним из классических примеров проблем синхронизации буфера. Синхронизация необходима для того, чтобы производитель прекратил производство, когда буфер заполнен, а потребитель прекратил удаление элементов из буфера, если он пуст. Вариации проблемы производитель-потребитель могут быть реализованы в различных типах приложений и может выполняться как на однопроцессорных, так и на многопроцессорных системах. В вычислении проблема производитель - потребитель (также известная как задача bounded-bu) является классическим примером задачи мультипроцессной синхронизации, первая версия которой была предложена Эдсгером Дийк в 1965 году в его unp manuscript, в которой bu был не связан, а впоследствии опубликован с bounded bu в 1972. В первой версии задачи присутствуют два циклических-процесса, общая доля которого, производитель и buu-que. Производитель повторно генерирует данные и записывает их в bu . Потребитель повторно читает данные в bu, переводя их в процессе чтения и используя эти данные каким-либо образом. В первом варианте проблемы, с несвязанным bu, проблема заключается в том, как сконструировать код производителя и потребителя так, чтобы при обмене данными они не были потеряны, данные считывались потребителем в том порядке, в каком они написаны производителем, и оба процесса достигали как можно большего прогресса. В более поздней формулировке проблемы, Эдсгер Дийк предложил несколько коллекцию производителей буферов. Это и потребителей, добавило разделяющих дополнительную конечную проблему предотвращения того, чтобы производители пытались записывать в буферы, когда все были заполнены, и попытки помешать потребителям считывать bu, когда все были пусты. Первым рассматриваемым случаем является тот, в котором есть один производитель и один потребитель, и есть буфер конечного размера. Решение для производителя - либо перейти в спящий режим, либо отбросить данные, если bu заполнен. В следующий раз, когда потребитель доставляет товар из bu, он будит спящего производителя, который снова начинает заполнять bu. Таким же образом, потребитель может уснуть, если обнаружит, что bu пуст. В следующий раз, когда производитель отправляет данные в bu, он будит спящего потребителя. Решение может быть достигнуто посредством межпроцессной связи, обычно с использованием семафоров. Несравнимое осуществление Для решения проблемы предлагается "решение", показанное ниже. В решении используются две библиотечные подпрограммы и. При вызове режима сна вызывающий абонент прерывается до тех пор, пока другой процесс не запустит его с помощью подпрограммы пробуждения. Глобальная переменная содержит количество элементов в bu. Проблема этого решения заключается в том, что оно содержит условие гонки, которое может привести к взаимоблокировке. Рассмотрим следующие сценарий: Только что прочитал переменную, заметил, что она равна нулю и только собирается двигаться внутри блока. Непосредственно перед вызовом сна потребитель прерывается, а производитель возвращается. Производитель создает элемент, вставляет его в bu и увеличивает itemCount на 1. Поскольку bu был пуст до последнего добавления, производитель пытается разбудить потребителя. К сожалению, потребитель еще не спал, и звонок пробуждения потерян. Когда потребитель возобновляет, он ложится спать и больше никогда не пробудится. Это происходит потому, что потребитель пробуждается производителем только тогда, когда itemCount равен 1. Продюсер будет петлять до тех пор, пока bu не заполнится, после чего также уйдет спать. Поскольку оба процесса будут спать вечно, мы попали в тупик. Поэтому это решение является ненасыщенным. Альтернативный анализ состоит в том, что если язык программирования не определяет семантику одновременных доступов к совместно используемым переменным (в данном случае) с использованием синхронизации, то решение по этой причине является ненасыщенным, не требуя точного демонстрации условия гонки. Использование семафоров Семафоры решают проблему потерянных вызовов пробуждения. В решении, приведенном ниже, мы используем два семафора и для решения проблемы. является количество элементов, уже в bu и доступно для чтения, в то время как является количество доступных пространств в bu где элементы могут быть записаны. Если производитель пытается уменьшиться, когда его значение равно нулю, производитель переводится в спящий режим. При следующем потреблении элемента происходит приращение, и производитель просыпается. Потребитель работает аналогично. < syntaxhighlight = "c" > semaphore fillCount = 0 ;//произведенные элементы semaphore empicalCount = BUEGLESS ;//оставшееся пространство производитель процедур {while (true) {item = Item ; down (empicalCount); putItemIntoBu (item); up (fillCount);} } procedure consumer {while (true) {down (fillCount); item = remeyItemStartBu ; up (empterCount); consumeItem (item);} } </syntaxhighlight > Решение выше работает хорошо, когда есть только один производитель и потребитель. Поскольку несколько производителей совместно используют одно и то же пространство памяти для элемента bu или несколько потребителей совместно используют одно и то же пространство памяти, это решение содержит серьезное условие гонки, которое может привести к двум или более процессам, считывающим или записывающим в одно и то же t одновременно. Чтобы понять, как это возможно, представьте, как процедура может быть реализована. Он может содержать два действия, одно, указывая следующий доступный t, а другое записывая в него. Если процедура может выполняться одновременно несколькими производителями, то возможны следующие сценарии: Сокращение двух производителей Один из продюсеров заносит следующую пустую t в буе. Второй производитель вводит следующую пустую t и получает тот же результат, что и первый производитель Оба производителя записывают в одну и ту же t Чтобы преодолеть эту проблему, нам нужен способ убедиться, что одновременно выполняется только один производитель. Другими словами, нам нужен способ выполнить критический раздел с взаимным. Решение для нескольких производителей и потребителей показано ниже. Обратите внимание, что порядок, в котором различные семафоры увеличиваются или уменьшаются, имеет важное значение: изменение порядка может привести к deadlock. Важно отметить, что, хотя мьютекс, кажется, работает как семафор со значением 1 (binary semaphore), но есть разница в том, что мьютекс имеет концепцию владения (ownership). Ownership означает, что мьютекс может быть "приращен" обратно (установлен на 1) тем же процессом, который "уменьшил" его (установлен на 0), а все остальные задачи ждут, пока мьютекс не будет доступен для уменьшения (фактически означает, что ресурс доступен), что вызывает взаимную блокировку (deadlock). Таким образом, использование мьютексов неправильно может остановить многие процессы, когда исключительный доступ не требуется, но мьютекс используется вместо семафора. В предыдущих главах рассмотрены способы синхронизации процессов, которые позволяют процессам успешно кооперироваться. Однако если средствами синхронизации пользоваться неосторожно, то могут возникнуть непредвиденные затруднения. Предположим, что несколько процессов конкурируют за обладание конечным числом ресурсов. Если запрашиваемый процессом ресурс недоступен, процесс переходит в состояние ожидания. В случае если требуемый ресурс удерживается другим ожидающим процессом, то первый процесс не сможет сменить свое состояние. Такая ситуация называется тупиком. Говорят, что в мультипрограммной системе процесс находится в состоянии тупика, дедлока (deadlock) или клинча, если он ожидает события, которое никогда не произойдет. Системная тупиковая ситуация или зависание системы является следствием того, что один или более процессов находятся в состоянии тупика. Рассмотрим пример. Предположим, что два процесса осуществляют вывод с ленты на принтер. Один из них успел монополизировать ленту и претендует на принтер, а другой наоборот. После этого оба процесса оказываются заблокированными в ожидании второго ресурса (см. рис. 7.1) Рис. 7.1. Пример тупиковой ситуации. Тупики также могут иметь место в ситуациях, не требующих выделенных ресурсов. Например, в системах управления базами данных процессы могут локализовывать записи, чтобы избежать гонок (см. главу "Синхронизация процессов"). В этом случае может получиться так, что один из процессов заблокировал записи, требуемые другому процессу и наоборот. Т.о. тупики могут иметь место, как на аппаратных, так и на программных ресурсах. Другой пример возникновение тупика в системах спулинга. Режим спулинга ввод-вывод с буферизацией информации, предназначенной для печати, на диске и организации очереди на печать часто применяется для повышения производительности системы. Программа, осуществляющая вывод на печать должна полностью сформировать свои выходные данные в промежуточном файле, после чего начинается реальная распечатка. В итоге, несколько заданий может оказаться в тупиковой ситуации, если предусмотренная емкость буфера для промежуточных файлов будет заполнена до того, как одно из заданий закончит свою работу. Возможные решения: увеличить размер буфера, или не принимать дополнительные задания, если файл спулинга близок к какому то порогу насыщения, например, заполнен на 75%. Определение. Множество процессов находится в тупиковой ситуации, если каждый процесс из множества ожидает события, которое только другой процесс данного множества может вызвать. Так как все процессы чего-то ожидают, то ни один из них не сможет инициировать событие, которое разбудило бы другого члена множества и, следовательно, все процессы будут спать вместе. Обычно событие, которого ждет процесс в тупиковой ситуации - освобождение ресурса. Далее в тексте данной главы будут обсуждаться вопросы, обнаружения, предотвращения, обхода тупиков Рассматривается также тесно и восстановления связанная проблема после тупиков. бесконечного откладывания, которое может происходить из-за дискриминационной политики планировщика ресурсов. Во многих случаях цена борьбы с тупиками, которую приходится платить высока. Тем не менее, для ряда систем, например для систем реального времени, нет иного выхода. Состояние соревнования. Процессы , работающие совместно могут сообща использовать некоторое хранилище данных. Каждый из процессов может считывать из общего хранилища данных и записывать туда информацию. Хранилищем, например, может быть файл общего доступа или участок оперативной памяти. Ситуации, в которых два (и более процесса считывают или записывают данные одновременно, и конечный результат зависит от того, какой из них был первым, называются ситуациями соревнования. Задачу упорядоченного доступа к разделяемым данным (устранение race condition), в том случае, если нам не важна его очередность, можно решить, если обеспечить каждому процессу эксклюзивное право доступа к этим данным. Каждый процесс, обращающийся к разделяемым ресурсам, исключает для всех других процессов возможность одновременного с ним общения с этими недетерминированному ресурсами, если поведению набора это может процессов. привести Такой к прием называется взаимоисключением (mutual exclusion). Если очередность доступа к разделяемым ресурсам важна для получения правильных результатов, то одними взаимоисключениями уже не обойтись. При сотрудничестве с использованием связи различные процессы принимают участие в общей работе, которая их объединяет. Связь обеспечивает возможность синхронизации, или координации, различных действий процессов. Обычно можно считать, что связь состоит из сообщений определенного вида. Примитивы для отправки и получения сообщений могут быть предоставлены языком программирования или ядром операционной системы. Поскольку в процессе передачи сообщений не происходит какого-либо совместного использования ресурсов, взаимоисключения не требуется, хотя проблемы взаимоблокировок и голодания остаются актуальными. Критические секции. Общепринятое взаимодействие между различными процессами АСИНХРОННОЕ взаимодействие, в противоположность СИНХРОННОМУ взаимодействию между различными модулями последовательно исполняемой программы. Проблема возникает только тогда, когда модификации подвергается объект , разделяемый несколькими процессами. Для возникновения проблемы достаточно, чтобы только один процесс занимался модификацией, а все остальные учитывали состояние объекта. Основной способ предотвращения проблем в этой и другой ситуации, связанной с совместным использованием памяти, файлов и чего-либо еще, является запрет одновременной записи и чтения разделенных данных более, чем одни процессом. То есть необходимо взаимное исключение. Это значит, когда один процесс использует разделенные данные, другому процессу это будет делать запрещено. Часть программы, в которой есть обращение к совместно используемым данным, называется критической областью или критической секцией. Если нам удается избежать одновременного нахождения двух процессов в критических областях, мы сможем избежать соревнований.. Для этого необходимо выполнение четырех условий: Два процесса не должны одновременно находится в критических областях; В программе не должно быть предположений о скорости или количестве процессов. Процесс, находящийся вне критической области, не может блокировать другие процессы. Невозможна ситуация, в которой процесс вечно ждет попадания в критическую область. 2. Методы синхронизации Взаимное исключение с активным ожиданием. Как возможный вариант – 1) запрет всех прерываний при входе процесса в критическую область, в том числе и прерываний по таймеру, то есть мы не сможем снять процесс с выполнения – отключить процессор. Однако давать такие полномочия пользовательскому процессу опасно. Если отключены все прерывания, а в результате сбоя не включить их обратно – операционная система разрушена. 2)Переменные блокировки. Для каждой критической области – одна совместно используемая переменная блокировки, так называемая, флаговая переменная. Процессы, прежде чем войти в критическую область, проверяют эту переменную. Недостаток: состязание процессов за переменную блокировки. Представьте, что один процесс считывает переменную блокировки, обнаруживает, что она равна 0, но прежде, чем он успевает изменить ее на 1, управление получает другой процесс, тоже изменяющий ее на 1. Когда первый процесс снова получит управление, он успешно заменит переменную блокировки на 1, и два процесса одновременно окажутся в критических областях. Листинг 4.1. Наивная реализация взаимного исключения на основе флаговой переменной. PR1: while(1){ while (turn!=0) ;/* loop*/ turn=1; critical_region (); turn=0; noncritical_region (); } PR2: while(1){ while (turn!=0) ; /* loop*/ turn=1; critical_region (); turn=0; noncritical_region (); } Именно благодаря простоте программа никуда не годится: проверка флага и его установка реализуются двумя разными операторами, в промежутке между которыми другой процесс может получить управление и также установить флаговую переменную! Окно, в котором происходит соревнование, составляет всего 2-3 команды, но при попадании обоих процессов в это окно, мы получаем как раз то, чего стремились избежать: оба процесса могут войти в критическую секцию. 3) Строгое чередование: while(1){ while (turn!=0)/* loop*/ critical_region (); turn=1; noncritical_region (); } Процесс 0 вначале проверяет значение turn, считывает 0 и входит в критическую область. while(1){ while (turn==0)/* loop*/ critical_region (); turn=0; noncritical_region (); } Процесс 1 также проверяет значение turn, считывает 0 и после этого входит в бесконечный цикл, ожидая когда же значение turn изменится. Постоянная проверка значения переменной называется активным ожиданием. Недостаток : бесцельная трата процессорного времени. Блокировка, использующая активное ожидание называется спин-блокировкой. Эта ситуация нарушает третье из сформулированных нами условий: один процесс блокирован другим, не находящимся в критической области. 4) Алгоритм Петерсона. #define FALSE 0 #define TRUE 1 #define N int turn; 2 /*количество процессов */ /*чья сейчас очередь */ int inerested[N]; /*все переменные изначально равны нулю */ void enter_region(int process); /*Процесс 0 или 1 */ { int other; /* номер второго процесса */ other =1- process; / * противоположный процесс */ interested[process] = TRUE; / * индикатор интереса*/ turn= process; / * установка флага*/ while (turn==process && interested[other] ==TRUE) / * Пустой оператор */ } void leave_region (int process) / * процесс, покидающий критическую область */ { interested[process] = FALSE; / * индикатор выхода из критической области */ } Исходно оба процесса находятся вне критических областей . Об этом говорит сайт https://intellect.icu . Процесс 0 вызывает enter_region , задает элементы массива устанавливает переменную turn равной 0. Поскольку процесс 1 не заинтересован в попадании в критическую область, процедура возвращается. Теперь, если процесс 1 вызовет enter_region, ему придется подождать, пока interested[0] примет значение FALSE, а это произойдет только в тот момент, когда процесс 0 вызовет процедуру leave_region, чтобы покинуть критическую область. Если оба сразу вызовут enter_region одновременно. Оба сохранят свои номера в turn. Сохранится номер того процесса, который был вторым, а предыдущий номер был утерян. Предположим, что вторым был процесс 1, так что значение turn равно 1. Когда оба процесса дойдут до оператора while, процесс 0 войдет в критическую область, а процесс1 останется в цикле и будет ждать, пока процесс 0 выйдет из критической области. 2. Примитивы межпроцессного взаимодействия Алгоритм Петерсона корректен, но он обладает одним недостатком – использованием активного ожидания. Реализует следующий алгоритм: перед входом в критическую область проверяет, можно ли это сделать. Если нельзя, то входит в тугой цикл ожидания – бесцельная растрата процессорного времени. . Семафоры. В 1965 году Дейкстра предложил использовать целую переменную для подсчета сигналов запуска . Этот новый тип переменных – семафор, значение может быть нулем или некоторым положительным числом. Две операции down и up. Операция down сравнивает значения семафора с нулем. Если значение семафора больше нуля, то down уменьшает его и возвращает управление. Если равно нулю, то процедура down не возвращает управления процессу, а переводит его в состояние ожидания. Операции: 1) проверки значения семафора; 2)изменения семафора или перевода процесса в состояние ожидания выполняются неделимое элементарное действие. Так как единое и называемое атомарное действие. Таким образом, за счет атомарности, гарантируется, что после начала операции ни один процесс не получит доступа к семафору до окончания или блокирования операции. Вообще говоря, группа операций модификации разделяемой структуры данных , которая происходит атомарно, не прерываясь никакими другими операциями с той же структурой данных, называется транзакцией. Операция up увеличивает значение семафора. Если с этим семафором связаны один семафора 1 или несколько ожидающих процессов, которые не могут завершить более раннюю операцию down, один из них выбирается системой. Таким образом, после выполнения операции up над семафором, связанным с несколькими ожидающими процессами, значение семафора так и останется равным 0, но число ожидающих процессов уменьшится на единицу. Операция увеличения семафора и активизации процесса тоже неделима. Ни один процесс не может быть блокирован во время выполнения операции up. Пытаясь пройти через семафор, процесс пытается вычесть из значения семафора 1 . Если значение семафора больше или равно 1, процесс проходит сквозь семафор успешно – семафор открыт. Если процесс равен нулю (семафор закрыт), процесс останавливается и становится в очередь. Закрытие семафора соответствует захвату или объекту или ресурса, доступ, к которому контролируется этим семафором. Если объект захвачен, то остальные процессы вынуждены ждать его освобождения. Закончив работу с объектом (выйдя из критической секции), процесс увеличивает значение семафора на единицу, открывая его. При первый, из стоявших в очереди процессов активизируется, вычитает из семафора единицу, снова закрывая семафор. Если же очередь была пуста, то ничего не происходит, просто семафор остается открытым. Это похоже на работу железнодорожного семафора, контролирующего движение по одноколейной ветке. Мьютексы. Mutual exclusion – взаимное исключение или двоичный семафор (0 и 1). Этот семафор соответствует случаю, когда с разделяемым ресурсом в каждый момент времени может работать только один процесс. Реализация его проста и эффективна, используется для синхронизации потоков. Значение мьютекса : mutex_lock и устанавливается двумя процедурами mutex_unlock . Семафоры общего вида. Могут принимать любые неотрицательные значения, еще называются счетчиками. Это соответствует случаю, когда несколько процессов могут работать с объектом одновременно или объект состоит из нескольких независимых, но равноценных частей - например, нескольких одинаковых принтеров. Проблема производителя и потребителя. Два процесса используют буфер ограниченного размера. Один из них, производитель, помещает туда данные, а другой потребитель – считывает их оттуда. Нужна переменная count для отслеживания количества элементов в буфере. Каждый из процессов должен проверять значение count: производитель, чтобы не переполнить буфер, а потребитель, чтобы уйти в состояние ожидание, если count=0. В случае необходимости один процесс активизирует другой. На уровне ОС неделимость операций down и up реализуется путем запрета прерываний. Поскольку для выполнения этих действий требуется всего лишь несколько команд процессора , запрет прерываний не приносит никакого вреда. Листинг 5.3. Проблема производителя и потребителя с семафорами #define N 100 /*количество сегментов в буфере*/ typedef int semaphore; /* семафоры – особый вид переменных*/ semaphore mutex =1; /* контроль доступа в критическую область*/ semaphore empty =N; /* число пустых сегментов буфера*/ semaphore full=0; /* число полных сегментов буфера*/ void producer (void) {int item; while (1){ item=pr(); /* создать данные помещаемые в буфер */ down(&empty); /* уменьшить счетчик пустых сегментов буфера*/ down(&mutex); /* вход в критическую область */ insert_item(item) /* поместить в буфер новый элемент */ up((&mutex); /*выход из критической области */ up(&full); /* увеличить счетчик полных сегментов буфера */ }} void consumer (void) {int item; while (1){ down(&full); /* уменьшить счетчик полных сегментов буфера*/ down(&mutex); /* вход в критическую область */ item=rm(); /* удалить элемент из буфера */ up((&mutex); *выход из критической области */ up(&empty); /* увеличить счетчик пустых сегментов буфера */ consum(); /* обработка элемента */ }} Семафоры использовались в программе двумя различными способами: 1) mutex – для взаимного исключения, а остальные семафоры использовались для синхронизации. В нашем случае они гарантируют, что производитель прекращает работу, когда буфер полон, а потребитель прекращает работу, когда буфер пуст. пример простейшей разделение ресурса синхронизации времени взаимодействующих (процессора). Можно процессовиспользовать семафорные операции для решения таких задач, в которых успешное завершение одного процесса связано с ожиданием завершения другого. Предположим, что существуют два процесса ПР1 и ПР2. Необходимо, чтобы процесс ПР1 запускал процесс ПР2 с ожиданием его выполнения, то есть ПР1 не продолжал бы выполняться до тех пор, пока процесс ПР2 до конца не выполнит свою работу. Пример синхронизации процессов var S: semaphore; begin Init Sem(S,0); ПР1: begin ПР11: {первая часть ПР1} ON (ПР2); {поставить на выполнение ПР2} P(S); ПР12; {оставшаяся часть ПР1} STOP end ПР2: begin ПР2: {вся работа программы ПР2} V(S); STOP end end Начальное значение семафора равно нулю. Если процесс ПР1 начал выполняться первым, то через некоторое время он поставит на выполнение процесс ПР2, после чего выполнит операцию P(S) и «заснет» на семафоре, перейдет в состояние пассивного ожидания. Процесс ПР2, осуществив все необходимые действия, выполнит примитив V(S) и откроет семафор, после чего процесс ПР1 будет готов к дальнейшему выполнению. решение задачи «читатели – писатели. Разделение ресурса места (памяти на жестком диске). «Читатели» - процессы, которые могут параллельно считывать информацию из некоторой общей области памяти, являющейся критическим ресурсом. «Писатели» - процессы, записывающие информацию в эту область памяти, исключая при этом и друг друга, и процессы «читатели». Имеются различные варианты взаимодействия между «писателями» и «читателями». Например, если хотя бы один «читатель» пользуется ресурсом, то он закрыт для использования всем «писателям» и доступен для использования всем «читателям». Во втором варианте, наоборот, больший приоритет у процессов «писатели». При появления запроса от «писателя» необходимо закрыть дальнейший доступ всем тем процессам «читателям», которые выдадут запрос на критический ресурс после него. Другим примером может служить система автоматизированной продажи билетов. Процессы «читатели» обеспечивают нас справочной информацией о наличии свободных билетов на тот или иной рейс. Процессы «писатели» запускаются с пульта кассира, когда он оформляет для нас билет. Имеется большое количество как «читателей» так и «писателей». Пример. Решение задачи «читатели – писатели» с приоритетом в доступе к критическому ресурсу «читателей». var R, W: semaphore; NR: Integer; procedure READER begin P(R); Inc(NR); {NR:=NR+1} if NR = 1 then P (W); V(R); Read_Data; {kritikal interval} P(R); Dec(NR); if NR=0 then V(W); V( R ); end procedure WRITER begin P(W); Write_Data; {kritikal interval} V(W) end begin NR:=0; Init Sem(R, 1); Init Sem(W, 1); begin while true do READER and while true do READER and while true do READER ……………………… and while true do WRITER and while true do WRITER end; end; При решении данной задачи используются два семафора R и W и переменная NR, предназначенная для подсчета текущего числа процессов типа «читатели», находящихся в критическом интервале. Доступ к разделяемой памяти осуществляется через семафор W. Семафор R используется для взаимоисключения процессов типа «читатели». Если критический ресурс не используется, то первый появившийся процесс при входе в критический интервал выполнит операцию P(W) и закроет семафор. Если процесс является «читателем», то переменная NR будет увеличена на единицу и последующие «читатели» будут обращаться к ресурсу, не проверяя значение семафора W, что обеспечивает параллельность их доступа к памяти. Последний «читатель», покидающий критический интервал, является единственным, кто выполнит операцию V(W) и откроет семафор W. Семафор R предохраняет от некорректного изменения значения NR, а также от выполнения «читателями» операций P(W) и V(W). Если в критическом интервале находится «писатель», то на семафоре W может быть заблокирован только один «читатель», все остальные будут блокироваться на семафоре R. Другие писатели блокируются на семафоре W. Когда «писатель» выполняет операцию V(W), неясно какого типа процесс войдет в критический интервал. Чтобы гарантировать процессам читателям получение более «свежей» информации необходимо при постановке в очередь готовности учитывать более высокий приоритет писателей. Однако этого оказывается недостаточно, ибо если в критическом интервале продолжает находиться, по крайней мере, один «читатель», то он не даст обновить данные, но и не воспрепятствует вновь приходящим процессам «читателям» войти в свою критическую секцию. Необходим дополнительный семафор.