Методы синхронизации процессов
Семафоры как общее средство синхронизации
Наиболее простой вид синхронизации действий, выполняемых в двух процессах, - это исполнение действия B в процессе Pj после того, как действие A исполнено в процессе Pi . Рассмотрим, как такую синхронизацию осуществить с помощью семафоров.
Используем семафор flag, инициализированный 0.
Код процесса Pi:
. . . A; signal (flag);
Код процесса Pj:
. . . wait (flag); B;
Общие и двоичные семафоры
Из рассмотренного ясно, что имеется два вида семафоров: общий - целая переменная с теоретически неограниченным значением - и двоичный - целая переменная, значениями которой могут быть только 0 или 1. Преимуществом двоичного семафора является его возможная более простая аппаратная реализация. Например, в системах "Эльбрус" и Burroughs 5000 реализованы команды атомарного семафорного считывания с проверкой семафорного бита.
Очевидно, что общий семафор может быть реализован с помощью двоичного семафора.
Вариант операции wait (S) для системных процессов ("Эльбрус")
Для системного процесса лишние прерывания нежелательны, и может оказаться важным удерживать процессор за собой некоторое время (например, для быстрого выполнения планирования и диспетчеризации процессов). С этой целью в системе "Эльбрус" реализована, в дополнение к операции ждать (S) русифицированной версии wait(S),операция жуж(S) - "жужжать" на процессоре, т.е. ждать на закрытом семафоре, но не прерываться и не отдавать процессор, пока семафор не будет открыт операцией открыть(S)
Реализация общего семафора с помощью двоичных семафоров
Общий семафор может быть представлен тройкой из двух двоичных семафоров и целой переменной:
binary-semaphore S1 = 1; binary-semaphore S2 = 0; int C = начальное значение общего семафора S;
Операция wait:
wait (S1); C--; if (C < 0) { signal (S1); wait (S2); } signal (S1);
Операция signal:
wait (S1); C++; if (C >= 0) { signal (S2); }; signal (S1);
В данной реализации семафор S1 используется для взаимного исключения доступа к общей целой переменной C. Семафор S2 используется для хранения очереди ждущих процессов в случае, если общий семафор переходит в закрытое состояние.
Решение классических задач синхронизации с помощью семафоров
Задача "ограниченный буфер".Имеются три классических задачи синхронизации процессов, решения которых с помощью семафоров мы рассмотрим:
- ограниченный буфер (bounded buffer problem)
- читатели – писатели (readers – writers problem)
- - обедающие философы (dining philosophers problem).
В данном разделе рассмотрим реализацию с помощью семафоров задачи ограниченный буфер:имеются процесс-производитель и процесс-потребитель, взаимодействующие с помощью циклического буфера ограниченной длины; производитель генерирует элементы информации и записывает в буфер; потребитель использует информационные элементы из буфера и удаляет их.
Будем использовать три общих семафора:
semaphore full = n; semaphore empty = 0; semaphore mutex = 1;
Семафор full сигнализирует о переполнении буфера, empty – об исчерпании буфера, mutex – используется для взаимного исключения действий над буфером.
Код процесса-производителя имеет вид:
do { . . . сгенерировать элемент в nextp . . . wait (full); wait (mutex); . . . добавить nextp к буферу . . . signal (mutex); signal (empty); } while (1);
Код процесса-потребителя:
do { wait (empty); wait (mutex); . . . взять и удалить элемент из буфера в nextc . . . signal (mutex); signal (full); . . . использовать элемент из nextc . . . } while (1);
Поясним использование семафоров. Семафор mutex используется "симметрично"; над ним выполняется пара операций: wait … signal – семафорные скобки. Его роль – чисто взаимное исключение критических секций. Семафор empty сигнализирует об исчерпании буфера. В начале он закрыт, так как элементов в буфере нет. Поэтому при закрытом семафоре empty потребитель вынужден ждать. Открывает семафор empty производитель, после того, как он записывает в буфер очередной элемент. Семафор full сигнализирует о переполнении буфера. В начале он равен n – максимальному числу элементов в буфере. Производитель перед записью элемента в буфер выполняет операцию wait (full),гарантируя, что, если буфер переполнен, записи нового элемента в буфер не будет. Открывает семафор full потребитель, после того, как он освободил очередной элемент буфера.
Заметим, что даже в такой сравнительно простой задаче использование семафоров весьма нетривиально и не вполне надежно – очень легко сделать ошибку. Стоит забыть открыть семафор, либо перепутать два семафора при операциях, и в программе может возникнуть взаимная блокировка процессов.