Методы синхронизации процессов
Алгоритм решения проблемы критической секции
Пусть для простоты имеется только два процесса – P0 и P1. Общая структура i - го процесса должна иметь вид:
do { вход в критическую секцию критическая секция выход из критической секции остальная часть кода } while (1)
Процессы могут использовать общие переменные для синхронизации своих действий.
Алгоритм 1.Предпримем первую попытку решения проблемы. Введем целую переменную turn (очередь), значение которой turn == i будет обозначать, что наступила очередь процесса номер i войти в свою критическую секцию. Первоначально turn == 0.
Алгоритм процесса Pi имеет вид:
do { while (turn != i); критическая секция turn = j; /* j != i: если i == 0, j == 1; если i == 1, j == 0 */ остальная часть кода } while (1)
Очевидно по построению, что данный алгоритм удовлетворяет принципу взаимное исключение. Однако он не удовлетворяет принципу прогресс: алгоритм не предпринимает никаких мер, чтобы ограничить время выбора процесса, желающего начать критическую секцию. Причина в следующем: алгоритм не хранит информацию о том, какие процессы желают войти в свои критические секции.
Алгоритм 2.Будем хранить не номер процесса, допущенного к критической секции, а массив булевских флагов flag [2], такой, что flag[i] == true, если i -й процесс готов войти в свою критическую секцию. Алгоритм процесса Pi примет вид:
do { flag[i] = true; while (flag[j]); /* j!=i: если i==0, j==1; если i == 1, j == 0 */ критическая секция flag[i] = false; остальная часть кода } while (1)
Данный вариант алгоритма также удовлетворяет принципу взаимного исключения, так как перед входом в критическую секцию процесс ждет, пока не останется других процессов, желающих войти в свои критические секции.
Однако данный алгоритм также не удовлетворяет принципу прогресс, Причина в том, что алгоритм не различает информацию о том, что процесс еще только готов войти в свою критическую секцию, и о том, что он в нее уже вошел.
Алгоритм 3.Модифицируем алгоритм, используя в нем одновременно и переменную turn, и массив флагов flag. Алгоритм процесса примет вид:
do { flag[i] = true; turn = j; while (flag[j] and turn == j); критическая секция flag[i] = false; остальная часть кода } while (1)
Можно проверить, что данный вариант алгоритма удовлетворяет всем трем принципам и решает проблему синхронизации по критическим секциям. Формальное доказательство предоставляем студентам. Идея данного варианта алгоритма в том, что перед входом в критическую секцию процесс сначала заявляет о своем намерении в нее войти, но затем пытается предоставить право на вход в критическую секцию другому процессу и только после того, как другой процесс ее выполнил и больше не желает в нее войти, входит сам в свою критическую секцию.
Алгоритм булочной (bakery algorithm)
Автор данного алгоритма – Л. Лампорт (L. Lamport). Рассмотрим другой алгоритм, решающий проблему синхронизации по критическим секциям. Происхождение названия следующее: алгоритм как бы воспроизводит стратегию автомата в (американской) булочной, где каждому клиенту присваивается его номер в очереди. В нашей российской реальности, данный алгоритм более уместно было бы назвать по этой же причине "алгоритм Сбербанка".
В алгоритме для n процессов используется булевский массив choosing[n]:значение choosing[i] == true будет означать, что в данный момент система определяет номер в очереди i -го процесса. Используется также целочисленный массив number[n]: number[i] будет обозначать вычисленный порядковый номер в очереди (приоритет) i- го процесса.
Алгоритм булочной (для i -го процесса) имеет вид:
do { choosing[i] = true; number [i] = max (number[0], number[1], …, number[n-1]) + 1; choosing[i] = false; for (j = 0; j < n; j++) { while (choosing[j]); while ((number[j] != 0) && (number[j] < number[i])); } критическая секция number [i] = 0; остальная часть кода } while (1)
По построению, номер, присваиваемый процессу, будет гарантированно больше, чем номер любого другого процесса в системе. Прежде чем войти в критическую секцию, процесс ждет, пока завершится процесс выбора номера для всех процессов и пока в системе есть хотя бы один выбранный процесс, номер которого меньше. По окончании критической секции процесс обнуляет свой номер. Данный алгоритм также решает проблему синхронизации процессов по критическим секциям.
Синхронизация на основе аппаратной поддержки атомарных операций
Рассмотренные алгоритмы синхронизации, не использующие каких-либо специальных синхронизирующих примитивов, достаточно сложны для понимания, разработки и сопровождения. Более простым (с точки зрения разработчика программ) решением для синхронизации была бы аппаратная и системная поддержка каких-либо простых атомарных операций, на основе которой реализовать синхронизацию процессов было бы проще.
Рассмотрим одну из этих операций, традиционно используемых для синхронизации, - операцию TestAndSet, которая атомарно выполняет считывание и запоминание значения переменной, затем изменяет его на заданное значение, но в результате выдает первоначальное значение переменной.
Предположим, что в системе имеется аппаратная поддержка следующей атомарной операции:
boolean TestAndSet (boolean & target) { boolean rv = target; target = true; return rv; }
С помощью данной операции реализовать синхронизацию процессов по критическим секциям очень просто. Введем в качестве блокировщика общую булевскую переменную:
boolean lock = false;
Код i -го процесса будет иметь вид:
do { while (TestAndSet (lock)); критическая секция lock = false; остальная часть кода } while (1)
Значение переменной lock, равное true,означает, что вход в критическую секцию заблокирован. Каждый процесс ждет, пока он не разблокируется, затем, в свою очередь, выполняет блокировку и входит в критическую секцию. При ее завершении процесс разблокирует критическую секцию присваиванием lock значения false.
Другое распространенное аппаратное решение для синхронизации – атомарная операция Swap, выполняющая перестановку значений двух переменных:
void Swap (Boolean * a, Boolean * b) { Boolean temp = * a; a = * b; * b = temp; }
Взаимное исключение по критическим секциям с помощью атомарной операции Swap реализуется следующим образом (приведен код i -го процесса) :
/* общие данные */ boolean lock = false; Boolean key = false; /* код процесса i */ do { key = true; while (key) { Swap (&lock, &key); } критическая секция lock = false; остальная часть кода } while (1)
При данной реализации, условием ожидания процесса перед входом в критическую секцию является условия (key == true),которое фактически означает то же, что и в предыдущей реализации, - закрытое состояние блокировщика, т.е., то, что другой процесс находится в своей критической секции. Когда критическая секция освободится (освобождение осуществляется присваиванием lock = false после завершения критической секции в исполнившем ее процессе), ее начнет исполнять текущий процесс.
Синхронизация на основе общих семафоров
Мы уже начали рассматривать семафоры Дейкстры как средство синхронизации в обзорной части курса. Здесь мы рассмотрим их более подробно в общем виде. Общий семафор (counting semaphore),по Э. Дейкстре, - это целая переменная S, над которой определены две атомарных семафорных операции wait (S) и signal (S) со следующей семантикой:
wait (S): while (S <= 0) do no-op; S--; signal (S): S++;
Фактически, если начальное значение общего семафора равно n (> 0), то это число задает количество процессов, которые могут беспрепятственно выполнить над семафором операцию wait.
Синхронизация по критическим секциям с помощью общего семафора осуществляется следующим образом:
/* общие данные */ semaphore mutex = 1; do { wait (mutex); критическая секция signal (mutex); остальная часть кода } while (1)
Реализация семафоров
Семафор, по существу, является структурой из двух полей – целого значения и указателя на список ждущих процессов:
typedef struct { int value; struct process * L; } semaphore;
При реализации операций над семафором будем предполагать наличие в системе следующих простейших примитивов и использовать их:
block - задерживает исполнение процесса, выполнившего эту операцию;
wakeup (P) – возобновляет исполнение приостановленного процесса P.
Определим семафорные операции следующим образом:
wait (S): S.value--; if (S.value < 0) { добавление текущего процесса к S.L; block; } signal (S): S.value++; if (S.value <= 0) { удаление процесса P из S.L; wakeup (P); }