
- •1. Процессы, коммуникация и координация в распределенных системах
- •1.1. Процессы
- •1.1.1. Структуры действий как процессы
- •1.1.2. Структурирование процессов
- •1.1.3. Последовательное представление процессов с помощью трасс
- •1.1.4. Рашуженис процесса на подпроцессы
- •1.1.5. Действия как переходы состояний
- •1,2. Описания систем через множество процессов
- •1.2.1. Сети Петри
- •1.2.2. Термы для описания процессов
- •1.2.3. Синхронизация и координация агентов
- •1.2.4. Предикаты над процессами
- •1.3. Языки программирования для описания взаимодействующих систем
- •1.3.1. Коммуникация через обмен сообщениями
- •1.3.2. Общие программные переменные
- •1.3.3. Языковые средства для параллельных ходов работы
- •1.3.4. Потоки ввода/вывода
- •2.1. Основные аспекты операционных систем
- •2.1.1. Функции операционной системы
- •2.1.2. Режимы обработки
1.3.2. Общие программные переменные
Одну из простейших и на практике наиболее употребительных концепций взаимного обмена информацией между параллельно работающими императивными программами представляют программные переменные, к которым взаимно обращаются эти программы.
Если в двух параллельно выполняющихся частях программы некоторая программная переменная встречается по меньшей мере в одной части программы в левой части присваивания, а также встречается и в другой части программы, то условие Бернштейна не выполняется. В этом случае говорят об общих программных переменных.
При этом, однако, могут возникнуть конфликты при параллельном обращении к общим переменным. В дальнейшем будут объяснены способы, позволяющие избегать так их конфликтов с помощью концепции взаимных исключений. Конфликтов при обращении к общим переменным для чтения или записи можно избежать с помощью взаимных исключений через охраняемые критические области.
Никаких конфликтов не возникает, если параллельно выполняемые программы используют исключительно различные переменные пли по меньшей мере удовлетворяют условию Бернштейна, согласно которому программная переменная, которая в одной из парахтельно выполняемых программ встречается в левой части оператора присваивания, не встречается в других параллельно выполняемых программах. Впрочем, программы, которые удовлетворяют этому условию и не используют операторы посылки/приема, не могут обмениваться сообщениями. Чтобы сделать возможным доступ к общим переменным из параллельно выполняющихся программ, но при этом предотвратить одновременные доступы к оAitoi'i и той же переменной, а эти доступы соответствующим образом координировать, используются охраняемые критические области.
Пусть Е - булевское выражение, a S - оператор или последовательность операторов. Если S может содержать чреватые конфликтами действия, то будем писать охраняемую критическую область в виде
await Е then S endwait чтобы выразить, что оператор S должен выполняться только при условии Е и при взаимном исключении других охраняемых критических областей. Условие Е назовем стражем (англ. guard), а оператор S - критической областью.
Упомянутая охрагаемая критическая область выполняется следующим образом. Осуществляется ожидание до тех пор, пока не будет достигнуто состояние, в котором выражение Е дает значение true и никакие другие параллельно протекающие процессы не выполняют как раз критическую область (взаимное исключение). Выполнение критической области состоит тогда в выполнении оператора S, причем параллельно выполняющиеся программы при их вступлении в критическую область должны ожидать завершения выполнения оператора S. Если состояние, в котором Е дает значение true, никогда не наступает, то выполнение охраняемой критической области не завершается, т. е. возникает локальный тупик. Таким образом, действия по выполнению охраняемой критической области соответствуют в ходе выполнения программы событиям, которые никогда не являются параллельными. Обеспечение взаимных исключений требует при выполнении программы наличия вышестоящего координирующего процесса.
Пример (параллельное вычисление факториала). Следующая программа вычисляет а! с помощью двух параллельных программ с общими переменными taken и z. При этом переменная z служит для обмена сообщениями, а переменная taken является вспомогательным средством для координации обменов сообщениями.
var bool taken, var nat у, var nat x := true, 1, n;
[Г while n > 0 do await taken then taken, z := false, n endwait;
n := n— 1
od
II
while x > 1 do await --taken then taken, x := true, z endwait;
у := y*x
od JJ
После выполнения программы справедливо высказывание у = n!. Z
С помощью критических областей можно избежать одновременных, сталкивающихся друг с другом обращений к общим переменным. При этом порядок выполнения присваиваний, а тем самым также и результат работы программы, не всегда устанавливается однозначно. Программы, как правило, являются недетерминистическими. '
Пример (недетерминизм в параллельных программах с критическими областями). Программа
[Г await true then z := 1 endwait || await true then z := 2 endwait jj
допускает выполнение
await true then z := 1 endwait; await true then z := 2 endwait
с конечным состоянием z = 2, а также выполнение
await true then z := 2 endwait; await true then z := I endwait
с конечным состоянием z = 1. С
Посылки и приемы сообщений с неявными буферами также могут быть описаны с помошыо обших переменных (мы используем тип и функции вычислительной структуры очередь (англ. queue, см. часть 1). Определим, что
channel m с есть var queue m с := emptyqueue,
send E on с есть await true then с := stock(c, E) endwait,
receive v on с есть await Hsempty(c) then v, с := firsl(c),
rest(c) endwait,
Это показывает, что буферизованные посылки/приемы сообщений также можно понимать как использование особой вычислительной структуры (а именно структуры очереди) в форме общих переменных.
Особые общие переменные являются семафорами - они служат исключительно для синхронизации процессов. Рааличают булевские и целочисленные семафоры. Целочисленный семафор s вводится в употребление с помощью объявления
sema nat х: = п.
Это равносильно объявлению программной переменной типа nat, с точностью до следующих ограничений на использование. Семафор s разрешается использовать только с помощью вызовов процедур P(s) и V(s), а не какими-либо присваиваниями или опросами. Процедуры Р и V определяются следующим образом:
proc Р = (sema nat х) : await х > 0 then х := х - 1 endwait, proc V = (sema nat x) : await true then x := x + 1 endwait.
Семафоры, в частности, используются для того, чтобы обеспечить взаимное исключение.
Пример (задача производитель/потребитель, координируемая с помощью семафоров). Следующая программа не удоалетворяет условию Бернштей- на (пусть b - любая булевская функция):
Г sema nat si, s2 := О, 1;
var ш x, у, z := х0, Уо, z0; (Г while -b(x) do P(sl);
x := у; V(s2); consume (x)
od
II
while -^b(z) do produce_next(z); P(s2); У := z; V(sl)
od JJ J
Переменная e используется в одной программе для чтения, а в другой - для записи. Однако благодаря использованию семафора одновременный доступ к общей переменной у исключается. С
При критических областях взаимные исключения дополнительно проверяются с помощью простого синтаксического условия (общие переменные встречаются только в охраняемых критических областях). В противоположность этому при недисциплинированном использовании семафоров проверка взаимных исключений для доступа к общим переменным представляет собой трудный семантический вопрос, так как обеспечение взаимного исключения может зависеть от сложных логических отношений программы.
Булевские семафоры вводятся с помощью объявления
sema bool s := b
и изменяются с помощью следующих процедур:
ргос Р = ( sema bool s ) . await s then s := false endwait, proc V = ( sema bool s ) : await true then s := true endwait.При использовании семафоров в незавершающихся программах снова ставится вопрос о справедливости выполнения операций над семафорами. При V-операциях можно без ограничений требовать, чтобы каждый вызов завершался успешно, однако это не имеет места дтя Р-оиерапий. Вызовы Р-операций могут приводить к ситуациям ожидания, если значение семафора есть 0, или false. Если одновременно ожидают многие Р-вызовы семафора, то после выполнения V-вызова продолжается один Р-вызов. Выбор подлежащего продолжению вызова происходит недетерминированно. При этом без предположения справедливости может случиться, что определенный Р-вызов никогда не будет продолжаться, хотя постоянно выполняются V-вызовы, поскольку всегда будут выбираться дтя продолжения другие ожидающие Р-вызовы. В этом случае говорят о старвации ("морении голодом", бесконечном ожидании; англ. starvation) вызова и соответствующей программы.
Старвапия имеет место, когда вызов (и соответственно, программа) должен ждать бесконечно долго, хотя все снова и снова возникают возможности продолжить выполнение программы. Если в (бесконечном) ходе работы старвации не возникает, то ход работы называется справедливым. В случае семафоров справедливые ходы работы могут быть реализованы путем введения очередей ожидания. К этому мы вернемся в следующей главе.
Вместо охраняемых критических областей находят также применение мониторы (Hoare, 1972; Brinch Hansen, 1972), чтобы координировать доступ к общим переменным. С помощью монитора осуществляется управление доступом к семейству общих переменных. Монитор включает в себя некоторое число функций, процедур и программных переменных. Процедуры доступны (видимы) извне и могут вызываться из парагтельно выполняющихся программ. Основное правило состоит в следующем: в мониторе в каждый момент времени может быть активным не более одного вызова процедуры (взаимные исключения). При этом вместо семафоров используются так называемые сигналы, которые очень похожи па булевские семафоры. С точки зрения обмена сообщениями сигналы дают нам возможность ожидать определенных простых сообщений.
Пример (задача производитель/потребитель с помощью монитора). Приводимый ниже монитор регулирует доступ к программной переменной q с помощью доступных извне процедур send и receive;
monitor EV =
Г var queue ш q; signal nonempty;
proc send = (m x) : Г q := stock(q, x); nonempty.signal J; proc receive = (var m y):
Г if isempty(q) then nonempty/wait fi;
q, у := rest(q), first(q) J;
q := emptyqueue; J;
var m x, z := xq, zo;
[Г while -'b(x) do EV.receive(x);
consume (x)
od
|| while ~,b(z) do produce_next(z);
EV.send(z)
od ' Л
Сигнал nonempty при этом соответствует булевской переменной, причем
nonempty.signal соответствует nonempty:=true; nonempty.wait соответствует nonempty:=fa!se;
await nonempty then nop endwait.
Обратим внимание, что взаимные исключения доступа к переменным монитора обеспечиваются концепцией монитора. А именно: при мониторах предполагается, что каждая из процедур при взаимных исключениях выполняется до тех пор, пока процедура успешно завершится или же по сигналу наступит ситуация ожидания. О
Наряду с описанными здесь существует много других более или менее равноценных предложений о языковых элементах для коммуникации и синхронизации операторов в параллельно выполняющихся программах. Однако они представляют собой лишь вариации обсужденных концепций.