Методы синхронизации процессов
Презентацию к данной лекции Вы можете скачать здесь.
Введение
В лекции рассматривается синхронизация процессов – одна из интереснейших и наиболее актуальных тем, в связи с широким распространением параллельных вычислительных систем и параллельных алгоритмов решения задач, требующих синхронизации параллельных процессов и потоков по общим ресурсам и событиям. В лекции рассмотрены следующие вопросы:
- История синхронизации процессов
- Проблема критической секции
- Аппаратная поддержка синхронизации
- Семафоры
- Классические проблемы синхронизации
- Критические области
- Мониторы
- Синхронизация в Solaris и в Windows.
История синхронизации
Методы синхронизации процессов рассматривались еще в 1960-х гг. в пионерской работе Э. Дейкстры [ 17 ] . Было отмечено, что совместный доступ параллельных процессов к общим данным может привести к нарушению их целостности. Поддержание целостности общих данных требует реализации и использования механизмов упорядочения работы взаимодействующих процессов (или потоков).
Анализ проблемы производитель – потребитель с точки зрения синхронизации по общему буферу
Вернемся к уже рассмотренной нами проблеме (парадигме) взаимодействия процессов производитель – потребитель (см. "Методы взаимодействия процессов" ). Имеется общий буфер ограниченной длины. Процесс-производитель добавляет в него сгенерированные элементы, процесс-потребитель использует и удаляет использованные элементы. Добавим в представление ограниченного буфера переменную counter,которую увеличивает процесс-производитель, добавляя очередной элемент к буферу, и уменьшает процесс-потребитель, используя и удаляя элемент из буфера.
Вспомним представление ограниченного буфера на языке Си (см. "Методы взаимодействия процессов" ) и расширим его переменной counter :
#define BUFFER_SIZE 1000 /* или другое конкретное значение */ typedef struct { . . . } item; item buffer[BUFFER_SIZE]; int in = 0; int out = 0; int counter = 0;
Теперь модифицируем реализации процесса-производителя и процесса-потребителя (см. "Методы взаимодействия процессов" ) , добавив соответствующие изменения переменной counter:
Процесс-производитель:
item nextProduced; /* следующий генерируемый элемент */ while (1) { /* бесконечный цикл */ while (counter == BUFFER_SIZE) ; /* ждать, пока буфер переполнен */ buffer[in] = nextProduced; /* генерация элемента */ in = (in + 1) % BUFFER_SIZE; counter++; }
Процесс-потребитель:
item nextConsumed; /* следующий используемый элемент */ while (1) { /* бесконечный цикл */ while (counter == 0) ; /* ждать, пока буфер пуст */ nextConsumed = buffer[out]; /* использование элемента */ out = (out + 1) % BUFFER_SIZE; counter--; }
Возникает вопрос: насколько корректны модифицированные алгоритмы, использующие переменную counter ? Не вполне. Проблема в том, что counter фактически является общим ресурсом, к которому одновременно обращаются два параллельных процесса. Если при этом обращение произойдет одновременно, то переменная counter может в итоге оказаться в некорректном состоянии. Поэтому необходимо, чтобы каждый из процессов при увеличении или уменьшении ее значения имел бы к ней монопольный доступ, и другой процесс не мог бы в это время "испортить" значение переменной. Иными словами, операции counter++ и counter— должны выполняться атомарно (см. лекцию 9). Напомним, что атомарная операция – это такая операция, которая должна быть выполнена полностью, без каких-либо прерываний; при этом операция, выполняемая одним из процессов, должна быть неделимой, с точки зрения другого процесса.
Операции count++ и count— могут быть реализованы на языке ассемблерного уровня следующим образом:
count++:
register1 = counter register1 = register1 + 1 counter = register1
count--:
register1 = counter register1 = register1 - 1 counter = register1
где register1 – регистр аппаратуры.
Проблема в том, что если и производитель, и потребитель пытаются изменить переменную counter одновременно, то указанные ассемблерные операторы тоже должны быть выполнены совместно (interleaved).
Конкретная реализация такого совместного выполнения зависит от того, каким образом происходит планирование для процессов – производителя и потребителя, а также от применения (или неприменения) в каждом из случаев аппаратных оптимизаций, например, увеличения (уменьшения) значения регистра одной командой за один такт ( increment / decrement).
Рассмотрим эффект interleaving на конкретном примере. Предположим, что начальное значение переменной counter в некоторый момент равно 5. Исполнение процессов в совместном режиме (interleaving) может привести к следующему эффекту:
производитель: register1 = counter (register1 = 5) производитель: register1 = register1 + 1 (register1 = 6) потребитель: register2 = counter (register2 = 5) потребитель: register2 = register2 – 1 (register2 = 4) производитель: counter = register1 (counter = 6) потребитель: counter = register2 (counter = 4)
Таким образом, значение counter в итоге может оказаться равным 6 или 4, в то время как правильное значение counter равно 5.
Ситуация, при которой взаимодействующие процессы могут параллельно (одновременно) обращаться к общим данным, называется конкуренцией за общие данные (race condition).Для предотвращения подобных ситуаций процессы следует синхронизировать.
Синхронизация процессов по критическим секциям
Рассмотрим проанализированную проблему в общем виде. Пусть имеются n параллельных процессов, каждый из которых может обратиться к общим для них данным. Назовем критической секцией фрагмент кода каждого процесса, в котором происходит обращение к общим данным. Проблема синхронизации процессов по критическим секциям заключается в том, чтобы обеспечить следующий режим выполнения: если один процесс вошел в свою критическую секцию, то до ее завершения никакой другой процесс не смог бы одновременно войти в свою критическую секцию.
Можно показать, что для решения проблемы критической секции необходимо и достаточно выполнение следующих трех условий:
- Взаимное исключение. Если некоторый процесс исполняет свою критическую секцию, то никакой другой процесс не должен в этот момент исполнять свою.
- Прогресс.Если в данный момент нет процессов, исполняющих критическую секцию, но есть несколько процессов, желающих начать исполнение критической секции, то выбор системой процесса, которому будет разрешен запуск критической секции, не может продолжаться бесконечно.
- Ограниченное ожидание.В системе должно существовать ограничение на число раз, которое процессам разрешено входить в свои критические секции, начиная от момента, когда некоторый процесс сделал запрос о входе в критическую секцию, и до момента, когда этот запрос удовлетворен.
При этом предполагается, что каждый процесс исполняется с ненулевой скоростью, но не делается никаких предположений о соотношении скоростей процессов.