
- •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.3. Языковые средства для параллельных ходов работы
Текстуальная нотация программ (в противоположность графическому представлению в виде блок-схем или сетям Петри) по существу является линейной. Несмотря на эту линейность нотации, она позволяет текстуально описывать параллельные процессы. Одну из таких возможностей предлагает уже применявшаяся нами в примерах парапельная композиция программ. Для программ Р1 и Р2 запись
(Г Р1 II Р2 jj
представляет программу, которая в качестве своего выполнения обладает параллельным ходом работы программы Р1 наряду с программой Р2. Для этого используется также синтаксис
parbegin PI par Р2 parend.
Поскольку параллельная композиция ассоциативна, часто пишут
itpjII -II p„il
вместо
[Гр,|| 1Гр2|| -fTPn-ill Pn-U - JJ JJ
Через рекурсивные вызовы процедур, которые содержат параллельные композиции, могут быть сформулированы системы со сколь угодно многими параллельно выполняющимися программами (неограниченная параллельность)
.Наряду с этой структурированной (ориентированной на скобки) возможностью определения параллельного выполнения в некоторых языках программирования мы находим операторы, позволяющие динамически создавать семейство параллельных процессов. Для этого используется специальный оператор, который напоминает оператор вызова процедуры, но здесь при выполнении вызова не ожидают окончания выполнения процедуры. Более того, после начата выполнения процедуры параллельно этому продолжается выполнение "главной программы". Мы говорим о расщеплении (англ. fork) потока управления.
Конечно, нужно позаботиться о том, чтобы параллельно протекающие выполнения, порожденные расщеплением, снова могли соединиться (англ. join).
Пример (образование параллельных процессов). Пусть р - описание программ!,! (или обозначение для такого описания), а х - идешификатор некоторого процесса (новый экземпляр выполнения программы). Оператор
start р паше х
порождает процесс, соответствующий заданному описанию р. Процесс получает индивидуальное имя х. Придание булевскому выражению
terminate х
значения (rue указывает на то, что процесс, обозначенный через х, уже закончен. Пока процесс протекает, булевское выражение лает значение false, С помощью
terminate х
процесс, обозначенный через х (и все индуцированные им процессы), заканчивается. Чтобы ввести обозначения (переменные) дтя самостоятельно выполняющихся программных единиц, используется объявление
process х.
Тем самым объявляется переменная х, при содействии которой позднее может быть указана ссылка на ход выполнения программы с помощью
start ... name х.
Программа, которая запускается в работу параллельно своему собственному выполнению (возникающий процесс обозначается через х), може* быть сформулирована с помощью приведенных выше операторов еле дуюшим образом:
Г process х ;
proc р =: ... ; {объяатение процедуры}
start р name х ; {начать параллельно протекающий процесс} if terminated х then {сбор, если процесс
не заканчивается}
else terminate х
f«; - J □
Мы можем также использовать пифическое представление для параллельного выполнения. Тогда в диаграмме выполнения с помощью конструкции
I
t I
обозначается соединение (слияние) этих потоков в один поток управления. После слияния управляющий поток продолжается только в случае, когда ;пя обеих ветвей управляющий поток готов к продолжению. Эти
графические изображения могут быть обобщены на расщепления и слияния с п управляющими потоками.
В заключение рассмотрим один весьма исчерпывающий пример параллельного выполнения программ. Мы дадим вариации последовательно
и параллельно выполняющихся программ для решения одной из поставленных задач.
Пример (поиск в дереве). Пусть заданы тип node вершин и тип tree двоичных деревьев с помощью объяатения
sort tree = empty J cons(tree left, node root, tree right) .
Пусть задано двоичное дерево t, в котором надо проверить, встречается
ли определенная вершина i. В программе будем использовать процедуру suche с заголовком процедуры
proc suche = (tree t, node i, var bool z)
,которая результирующей переменной z присваивает значение true, если в двоичном дереве t имеется вершина, помеченная через i, и значение false в противном случае. Последовательная программа, которая решает поставленную задачу, выглядит следующим образом:
proc suche = (tree t, node i, var boo) z):
if -nsempty(t)
then if root(t) = i then z := true
else var bool zl, zr;
suche(left(t), i, zl); suche(right(t), i, zr); z := zl v zr
П
else z .= false fi.
Первая простая программа, которая работает с независимыми параллельными поисками, гласит:
proc suche = (tree t, node i, var bool z): if ~4.sempty(t) then if root(t) = i
then z := true
else var bool zl, zr := false, false;
ff suche(lefl(t), i, zl) || suche(right(t), i, zr) JJ; z := zl v zr
fi
elsez := false fi.
Вызов процедуры suche(t, i, z)
создает каскад параллельно выполняемых вызовов процедуры suche, по одному на каждую вершину. После того как выполнение всех вызовов будет завершено, переменная z содержит искомый результат.
Впрочем, при таком предписании, как правило, будет дальше продолжаться параллельный поиск, лаже в том случае, когда уже будет установлено, что результатом является true. Эту неэффективность можно устранить путем использования программной структуры, в которой параллельные процессы поиска обмениваются сообщениями. Если применить общую переменную z, которой первоначально присваивается значение false, то получим следующую процедуру
:proc suche = (Iree t, node i, var bool z)
if Hscmpty(t)
then if root(t) = i
then await true then z := true endwait else var bool b;
await true then b := z endwait; if -b then IT suche(left(t), i, z) || suche(right(t), i, z) jj else nop
fi
fi
else nop fi.
С помощью объявления и последующего вызова
var bool z := false; suche(t, i, z)
порождается каскад параллельно выполняющихся программ. После того как все эти профаммы завершат работу, переменная z будет содержать искомый результат. Как только один из процессов найдет искомую вершин)1, другие процессы прекратят поиск. Однако можно обойтись и без обшей переменной, в которую записывают параллельно выполняющиеся программы. Это демонстрируется следующим предписанием, которое работает с образованием процессов:
proc suche = (tree t, node i, var bool z): • if -"isempty(t) then if root(i)
then process pi, pr;
var bool zl, zr := false, false;
start sue he (left (i), i, zl) name pi;
start siiche(right(!), i, zr) name pr;
await zl v zr v (terminated pi л terminated pr)
then z :== zl v zr endwait; if --terminated pi then terminate pi fi; if --terminated pr then terminate pr fi else z := true fi
else z := false fi.
Поиск в дереве t вершины i ориентируется на структуру дерева. При этом порождается дерево процессов. Если процесс-потомок i находит искомуто вершину в корне своего дерева, то этот результат передается наверх.
В ином случае, т. е. если дерево не пустое, дтя каждого поддерева запускается параллельный про а есс для поиска. □
Для описания систем параллельно выполняющихся процессов существует много, часто труднообозримых, механизмов. Однако они могут быть сведены к ранее описанным конструктам или, соответственно, могут быть объяснены как их расширение. Особенно трудны вопросы обеспечения корректности в разработке, верификации и доказательстве требуемых свойств для параллельных программных систем.
Обратим внимание, что при обсуждении процессов мы отказались от количественных временных аспектов (как, например, "нужно ожидать 5 секунд"). Количественно критичные по времени процессы и, соответственно, программы, которые описывают такие процессы, требуют собственных концепций, которые, однако, родственны обсуждавшимся языковым элементам. Такие программы используются в режиме реального времени для управления реальными процессами.