5.8 Алгоритм Деккера
На рис. 5.3 показана первая версия программного кода для реализации взаимоисключения в контексте параллельной программы с двумя процессами. Конструкция parbegin/parend позволяет организовать параллельную работу процессов «процессодин» и «процессдва». Каждый из этих процессов представляет собой бесконечный цикл с многократным вхождением в свой критический участок. В программе рис. 5.3 примитив «входвзаимоисключения» реализуется как один цикл while, который повторяется до тех пор, пока переменная «номер процесса» не станет равной номеру данного процесса, примитив «выходвзаимоисключения» реализуется как одна команда, которая устанавливает для переменной «номерпроцесса» значение, равное номеру другого процесса.
«Процессодин» выполняет свой цикл. Поскольку первоначально переменная «номер процесса» имеет значение 1, «процессодин» входит в свой критический участок. «Процессдва» обнаруживает, что «номерпроцесса» равен 1 и остается заблокированным на своем цикле while do. Если «процессдва» получает в свое распоряжение процессор, он просто выполняет цикл ожидания момента, когда для переменной «номерпроцесса» будет установлено значение 2, т. е. «процессдва» не может войти в свой критический участок и тем самым гарантируется взаимоисключение.
В конце концов «процессодин» закончит работу в своем критическом участке (мы должны предполагать, что здесь нет бесконечных циклов) и установит «номерпроцесса» равным 2, так что «процессдва» получит возможность входа в свой критический участок.
program версияодин;
var номерпроцесса: целое;
procedure процессодин;
begin
while истина begin
while номерпроцесса = 2 do;
критическийучастокодин;
номерпроцесса : = 2;
прочиеоператорыодин
end
end;
procedure процессдва;
begin
while истина do begin
while номерпроцесса = 1 do;
критическийучастокдва;
номерпроцесса : = 1;
прочиеоператорыдва
end
end;
begin
номерпроцесса := 1;
parbegin
процессодин;
процессдва parend
end;
Рис. 5.3 Программная реализация примитивов взаимоисключения (версия 1)
Программа рис. 5.3 гарантирует взаимоисключение, однако весьма дорогой ценой.
«Процессодин» должен выполняться первым, так что если «процессдва» готов к входу в свой критический участок, он может получить разрешение на это со значительной задержкой.
После того как «процессодин» войдет в свой критический участок и затем выйдет из него, должен будет выполняться «процессдва», даже если «процессодин» хочет вновь войти в свой критический участок, а «процессдва» еще не готов.
Таким образом, процессы должны входить и выходить из своих критических участков строго поочередно.
Если одному процессу приходится это делать во много раз чаще, чем другому, то он вынужден работать с гораздо меньшей скоростью, чем это необходимо.
Подобная программа не может оказаться в состоянии полного тупика; если оба процесса одновременно пытаются войти в свои критические участки, то, по крайней мере, одному из них удастся продолжить работу.
А если один из процессов завершится, то со временем другой окажется не в состоянии продолжать выполнение.
program версиядва;
var п1внутри, п2внутри: логический;
procedure процессодин;
begin
while истина do
begin
while п2внутри do;
п1внутри : = истина;
критическийучастокодин;
п1внутри : = ложь;
прочиеоператорыодин
end
end;
procedure процессдва;
begin
while истина do
begin
while п1внутри do;
п2внутри : = истина;
критическийучастокдва;
п2внутри : = ложь;
прочиеоператорыдва
end
end;
begin
п1внутри: = ложь;
п2внутри: = ложь;
parbegin
процессодин;
процессдва
parend
end;
Рис. 5.4 Программная реализация примитивов взаимоисключения (версия 2).
В первой версии программной реализации взаимоисключения имеется только одна глобальная переменная и, таким образом, возникает проблема жесткой синхронизации. Поэтому во второй версии (рис. 5.4) мы используем две переменные — «п1внутри» и «п2внутри», которые имеют истинное значение, если «процессодин» и «процессдва» соответственно находятся внутри своих критических участков.
В этой версии программы «процессодин» остается в состоянии активного ожидания (busy wait) до тех пор, пока «п2внутри» имеет значение «истина».
В конце концов «процессдва» выходит из своего критического участка и выполняет собственный код «выходвзаимо-исключения», устанавливая для переменной «п2внутри» значение «ложь».
После этого «процессодин» устанавливает для переменной «п1внутри» значение «истина» и входит в свой критический участок.
Когда переменная «п1внутри» имеет значение «истина», «процессдва» в свой критический участок войти не может.
Здесь опять-таки выявляются некоторые нюансы, связанные со спецификой параллельного программирования. Поскольку «процессодин» и «процессдва» являются параллельными процессами, они оба могут одновременно попытаться начать выполнять свои входные последовательности взаимоисключения. Вначале «п1внутри» и «п2внутри» имеют значение «ложь».
«Процессодин» может проверить переменную «п2внутри» и обнаружить, что она имеет значение «ложь», а затем, еще до того, как «процессодин» успеет установить для переменной «п1внутри» значение «истина», «процессдва» может проверить переменную «п1внутри» и обнаружить «ложь».
В этот момент «процессодин» установит истинное значение для «п1 внутри» и войдет в свой критический участок, и «процессдва» установит истинное значение для «п2внутри» и войдет в свой критический участок.
При этом оба процесса оказываются в своих критических участках одновременно, так что программа версии 2 даже не гарантирует взаимоисключения.
Слабым местом программы версии 2 является то, что между моментом, когда процесс, находящийся в цикле ожидания, определяется, то он может идти дальше, и моментом, когда этот процесс устанавливает флаг-признак, говорящий о том, что он вошел в свой критический участок, проходит достаточно времени, чтобы другой процесс успел проверить еще не установленный флаг и войти в свой критический участок.
Поэтому необходимо, чтобы, в то время как один процесс выполняет свой цикл ожидания, другой процесс не мог выйти из своего собственного цикла ожидания. В программе версии 3 (рис. 5.5) для решения этой проблемы предусматривается установка каждым процессом своего собственного флага перед выполнением цикла ожидания.
Программа версии 3 позволила решить одну задачу, однако сразу же появилась другая. Если каждый процесс перед переходом на цикл проверки будет устанавливать свой флаг, то каждый процесс будет обнаруживать, что флаг другого процесса установлен я будет бесконечно оставаться в цикле while. Эта версия программа может служить примером тупика для двух процессов.
Главный недостаток программы версии 3 заключается в том, что каждый из процессов может заблокироваться в своем цикле ожидания while. Нам нужен способ «разорвать» эти циклы.
program версиятри;
var п1хочетвойти, п2хочетвойти: логический;
procedure процессодин;
begin
while истина do begin
п1хочетвойти : = истина;
while п2хочетвойти do;
критическийучастокодин;
п1хочетвойти : = ложь;
прочиеоператорыодин
end
end;
procedure процессдва;
begin
while истина do begin
п2хочетвойти : = истина;
while п1хочетвойти do;
критическийучастокдва;
п2хочетвойти : = ложь;
прочиеоператорыдва
end
end;
begin
п1хочетвойти : = ложь;
п2хочетвойти : = ложь;
parbegin
процессодин;
процессдва;
parend end;
Рис. 5.5 Программная реализация примитивов взаимоисключения (версия 3).
В версии 4 (рис. 5.6) для этого предусматривается периодическая кратковременная установка ложного значения флага каждым вошедшим в цикл процессом. Благодаря этому другой процесс получает возможность выйти из своего цикла ожидания при по-прежнему установленном собственном флаге.
program версиячетыре;
var п1хочетвойти, п2хочетвойти: логический:
procedure процессодин;
begin
while истина do begin
п1хочетвойти : = истина
while п2хочетвойти do begin
п1хочетвойти : = ложь;
задержка (случайная, несколькотактов);
п1хочетвойти : = истина
end
критическийучастокодин;
п1хочетвойти : = ложь;
прочиеоператорыодин
end
end;
procedure процессдва;
begin
while истина do begin
п2хочетвойти : = истина;
while п1хочетвойти do begin
п2хочетвойти : = ложь;
задержка (случайная, несколькотактов);
п2хочетвойти : = истина
end
критическийучастокдва;
п2хочетвойти : = ложь;
прочиеоператорыдва
end
end;
begin
п1хочетвойти : = ложь;
п2хочетвойти : = ложь;
parbegin
процессодин;
процессдва parend
end;
Рис. 5.6 Программная реализация примитивов взаимоисключения (версия 4).
program алгоритмДеккера;
var
избранныйпроцесс: (первый, второй);
п 1 хочетвойти, п2хочетвойти: логический;
procedure процессодин;
begin
while истина do begin
п1хочетвойти : = истина;
while п2хочетвойти do
if избранныйпроцесс = второй then
begin
п1 хочетвойти : = ложь;
while избранныйпроцесс = второй do;
п1хочетвойти : = истина
end
критическийучастокодин;
избранныйпроцесс : = второй;
п1 хочетвойти : = ложь;
прочиеоператорыодин
end
end;
procedure процессдва;
begin
while истина do begin
п2хочетвойти : = истина
while п1 хочетвойти do
if избранныйпроцесс = первый then begin
п2хочетвойти : = ложь;
while избранныйпроцесс = первый do;
п2хочетвойти : = истина
end
критическийучастокдва;
избранныйпроцесс : = первый;
п2хочетвойти : = ложь;
прочиеоператорыдва
end
end;
begin
п1 хочетвойти : = ложь;
п2хочетвойти : = ложь;
избранныйпроцесс : = первый;
parbegin
процессодин;
процессдва
parend
end;
Рис. 5.7. Реализация примитивов взаимоисключения согласно алгоритму Деккера.
В программе версии 4 гарантируется взаимоисключение и отсутствие тупика, однако возникает другая потенциальная проблема, также очень неприятная, а именно бесконечное откладывание.
Рассмотрим, каким образом это происходит.
Поскольку мы не можем делать никаких предположений об относительных скоростях асинхронных параллельных процессов, мы должны проанализировать все возможные последовательности выполнения программы.
Процессы здесь могут, например, выполняться «тандемом», друг за другом в следующей последовательности: каждый процесс может установить истинное значение своего флага; произвести проверку в начале цикла; войти в тело цикла; установить ложное значение своего флага; снова установить истинное значение флага; повторить всю эту последовательность, начиная с проверки при входе в цикл.
Когда процессы будут выполнять все эти действия, условия проверки будут оставаться истинными. Естественно, подобный режим работы весьма маловероятен — однако все же в принципе возможен. Поэтому версия 4 также оказывается неприемлемой.
Программу, реализующую взаимоисключение таким способом, нельзя применять, например, в системе управления космическими полетами, управления воздушным движением или в водителе ритма сердца человека, где даже малая вероятность бесконечного откладывания процесса и последующий отказ системы в целом категорически недопустимы.
Алгоритм, предложенный Деккером, позволяет при помощи небольшого по объему программного кода (рис. 5.7) изящно решить проблему взаимоисключения для двух процессов, не требуя при этом никаких специальных аппаратно-реализованных команд.
Алгоритм Деккера исключает возможность бесконечного откладывания процессов, из-за которых программа версии 4 неприемлема.
Рассмотрим, каким образом это делается.
Процесс «п1» уведомляет о желании войти в свой критический участок, устанавливая свой флаг. Затем он переходит к циклу, в котором проверяет, ре хочет ли также войти в свой критический участок и «п2». Если флаг «п2» не установлен, то «п1» пропускает тело цикла ожидания и входит в свой критический участок.
Предположим, однако, что «п1» при выполнении цикла проверки обнаруживает, что флаг «п2» установлен.
Это заставляет «п1» войти в тело своего цикла ожидания.
Здесь он анализирует значение переменной «избранныйпроцесс», которая используется для разрешения конфликтов, возникающих в случае, когда оба процесса одновременно хотят войти в свой критический участок. Если избранным процессом является «п1», он пропускает тело своего цикла if и повторно выполняет цикл проверки в ожидании момента, когда «п2» сбросит свой флаг. (Мы вскоре увидим, что «п2» со временем должен это сделать.)
Если процесс «п1» определяет, что преимущественное право принадлежит процессу «п2», он входит в тело своего цикла if, где сбрасывает свой собственный флаг, а затем блокируется в цикле ожидания, пока избранным процессом остается «п2». Сбрасывая свой флаг, «п1» дает возможность «п2» войти в свой критический участок.
Со временем «п2» выйдет из своего критического участка и выполнит свой код «выходвзаимоисключения». Операторы этого кода обеспечат возврат преимущественного права процессу «п1» и сброс флага «п2». Теперь у «п1» появляется возможность выйти из внутреннего цикла ожидания while и установить собственный флаг. Затем «п1» выполняет внешний цикл проверки. Если флаг «п2» (недавно сброшенный) по-прежнему сброшен, то «п1» входит в свой критический участок. Если, однако, «п2» сразу же пытается вновь войти в свой критический участок, то его флаг будет установлен, и «п1» снова придется войти в тело внешнего цикла while. Однако на этот раз «бразды правления» находятся уже у процесса «п1», поскольку сейчас именно он является избранным процессом (напомним, что «п2», выходя из своего критического участка, установил для переменной «избранный процесс» значение «первый»). Поэтому «п1» пропускает тело условной конструкции if и многократно выполняет внешний цикл проверки, пока «п2» «смиренно» не сбросит собственный флаг, позволяя процессу «п1» войти в свой критический участок.
