В.Ш. Кауфман - Языки программирования - концепции и принципы (1990) (1160787), страница 29
Текст из файла (страница 29)
package общий is
...
b: буфер; - для сообщений
s: семафор;
неполон, непуст: сигнал; -- сигналы-будильники
procedure поставить (X: in сообщение);
procedure получить (X: out сообщение);
function полон ...;
function пуст...;
end общий;
task поставщик is task потребитель is
X: сообщение; X: сообщение;
. . . . . .
loop -- бесконечный цикл! loop
выработать (X); оградить (S);
оградить (S); -- для себя if пуст then
if полон then освободить (S);
освободить(S); -- для потр. ждать (непуст);
ждать (неполон); -- от потр. оградить (S);
оградить (S); -- для себя end if;
end if; получить (X);
поставить (X); освободить (S);
освободить (S); потребить (X);
послать (непуст); послать (неполон);
end loop; end loop;
. . . . . .
end поставщик; end потребитель;
Полезно напомнить следующие существенные моменты.
1. Условный оператор обеспечивает развязку. Без него была бы фактически
полная синхронизация.
2. Ограждение проверки нужно для монополизации разделяемого ресурса, а
освобождение - чтобы избежать тупика. Ведь ожидаемый сигнал может прийти
только от партнера - надо дать последнему возможность работать с буфером.
При этом первый же цикл партнера обязательно даст такой сигнал, так что
"застрять" на ожидании нельзя.
3. Семафор обеспечивает поочередную работу процессов - он не может
быстро мигать в результате работы только одного процесса, когда второй ждет
у семафора. При первом же освобождении пойдет второй процесс и первый будет
ждать у своего "оградить", если первым до него доберется.
4. Целостность объектов в нашей программе не обеспечена - связь
семафора с буфером, а также сигнала с буфером никак в программе не отражена
- отражена лишь в мыслях программиста. Это неадекватно (не отражает суть
дела) и опасно, так как нет контроля за этими связями. Другими словами,
свойства семафоров и сигналов как языковых конструктов не соответствуют
основному критерию качества ЯП (усложняют программирование и понимание
программ).
5. Структурно не выделены части программы, существенно зависящие от
взаимодействия с другими процессами, от частей, в которых можно
абстрагироваться от такого взаимодействия (и самого факта управления одним
из членов коллектива асинхронных процессов). Средства программирования
таковы, что при написании буквально любой команды следует опасаться
"подводных камней" параллелизма.
6.4. Концепция внешней дисциплины.
Частично резюмируя пункты 4 и 5, частично обобщая их, можно сделать
вывод об использовании в нашей программе (сознательно или интуитивно) так
называемой "концепции внешней дисциплины" взаимодействия процессов. Термин
"внешней" отражает отношение дисциплины к разделяемым процессами ресурсам.
Суть этой концепции в том, что о дисциплине (правилах) использования
разделяемых ресурсов должны заботиться сами процессы-партнеры. Другими
словами, она "локализована" вне общих ресурсов. Примером такой дисциплины
может служить "скобочное" правило применения операций "оградить" и
"освободить".
Итак, мы рассмотрели пример задачи поставщик-потребитель и показали,
как в рамках концепции внешней дисциплины при обмене справиться с проблемой
порчи данных (немонопольный, множественный доступ), а при синхронизации - с
проблемой порчи управления (тупики и лишние ожидания). При этом мы
совершенно игнорируем запуск и завершение процессов.
[Полезно понимать, что взаимно-дополнительные свойства семафоров и
сигналов позволяет сводить использование семафоров к использованию сигналов
и наоборот. Однако если при этом применение сигналов вместо семафоров
допускает толкование, вполне согласующееся с названием соответствующих
операций, то обратное неверно. Именно парно-скобочное применение операций
оградить (S) ... освободить (S) естественно трактовать как
ждать (разрешение_ввести_в_критический_участок_для_S) и
послать (сигнал_о_завершении_критического_участка_для_S),
где в скобках указаны два сигнала, соответственно посылаемые и ожидаемые
внешней (управляющей процессами) средой (точнее, некоторым процессом-
диспетчером), в которой находится пара операторов
послать (сигнал_о_завершении_критического_участка_для_S) и
ждать (разрешение_ввести_в_критический_участок_для_S).
Так что один семафор сводится к двум сигналам. Свести к одному опасно!
Иначе диспетчер будет равноправен с другими процессами. Здесь же только он
имеет право послать (разрешение...).
Невозможность обратной замены (сигналов на семафоры) с сохранением
"скобочного" смысла операции очевидна - ведь в процессе может оказаться
только одна из таких операций. Однако если не сохранять симметрию операций в
одном процессе, то и здесь заменяющее истолкование возможно: "ждать"
трактуется как "оградить" последующие операторы, пока не будет сигнала (но
не от вмешательства, а от исполнения), а "послать" - как "освободить" от
вынужденного ожидания].
6.5. Концепция внутренней дисциплины: мониторы
Замысел внутренней дисциплины вполне укладывается в идеологию РОРИУС:
разделяемый ресурс следует представить некоторым специальным комплексом
услуг, реализация которого концентрирует в себе все особенности параллелизма
и конкретной операционной среды, а использование становится формально
совершенно независимым от поведения и даже наличия процессов-партнеров.
Здесь слово "формально" подчеркивает факт, что в процессе-пользователе
оказывается невозможным обнаружить какие-либо следы присутствия процессов-
партнеров. Более того, его семантика полностью описывается в терминах
взаимодействия с одним только указанным специальным комплексом услуг. Хотя
конечно, эти услуги содержательно связаны с наличием и функционированием
процессов-партнеров.
Итак, концепция внутренней дисциплины состоит в локализации всех
средств управления взаимодействием коллектива процессов в рамках некоторого
явно выделенного разделяемого ресурса - специального комплекса услуг,
называемого монитором.
Мониторы могут быть весьма разнообразными. Содержательно близкие роли
играют диспетчеры (супервизоры) операционных систем, но их далеко не всегда
сознательно проектируют в рамках концепции внутренней дисциплины. Мы
рассмотрим некоторую полезную абстракцию, так называемые мониторы Хансена-
Хоара, предложенные в 1973-1975 гг.
Продолжим рассматривать нашу задачу-пример о поставщике и потребителе.
Ключевая идея: разделяемый ресурс (буфер) следует превратить в комплекс
услуг, самостоятельно обеспечивающий корректность доступа к буферу. С
подобной идеей мы уже знакомились в последовательном программировании -
пакеты предоставляют комплекс услуг с защитой внутренних ресурсов от
нежелательного доступа.
Теперь требуется аналог пакета в ЯПП. Пакет был в состоянии защитить
ресурс за счет того, что доступ допускался только через разрешенные
операции. Причем поскольку исполняемый процесс был единственным, всегда
исполнялась единственная операция пакета. Если это фундаментальное свойство
пакета сохранить, то для корректности доступа к ресурсу совершенно не
существенно, сколько и каких операций выполняется вне пакета.
Таким образом, пакет с указанным встроенным фундаментальным свойством
операций (встроенным по семантике соответствующего ЯПП) пригоден и для
обслуживания асинхронных процессов-пользователей. Такой пакет и называется
монитором Хансена-Хоара. (Он был воплощен, в частности, в ЯП Параллельный
Паскаль Бринча Хансена.)
Другими словами, монитор - это пакет со встроенным режимом взаимного
исключения предоставляемых пользователю операций. Тем самым реализуется
монопольный доступ процесса к ресурсу без каких-либо специальных указаний со
стороны этого процесса (и усилий программиста).
Полезно осознать, что здесь мы в очередной раз имеем дело с
рациональной абстракцией и удобной конкретизацией. Монитор позволяет
пользователю отвлечься от "параллельной природы" использования ресурса и
работать с ним в обычном последовательном (монопольном) режиме. Вместе с тем
он позволяет программисту полностью учесть "природу" конкретного ресурса и
доступа к нему при создании тела монитора.
Сказанное следует понимать именно так, что пользователь может
программировать, полностью игнорируя параллелизм процессов. Этот идеал
пользования разделяемым ресурсом-буфером в случае нашего примера выглядит
так:
with буф; use буф; with буф; use буф;
task поставщик is task потребитель is
. . . . . .
loop loop
выработать (X); || полная || получить (X);
поставить (X); || абстракция || потребить (X);
end loop; || от способа || end loop;
end поставщик; || обмена || end потребитель;
Здесь "буф" обозначает нужный контекст. Им и служит монитор,
предоставляющий необходимые услуги.
Обратите внимание: наш идеал в точности совпадает с первоначальным
замыслом, прямое воплощение которого было неработоспособно! Такой возврат
свидетельствует об очевидном прогрессе - в программе пользователя нет ничего
лишнего!
Перейдем к реализации монитора:
with общий; use общий; -- чтобы не переписывать
monitor буф is -- !! в Аде такого нет, продолжаем писать на
entry поставить (X : in сообщение); -- Ада-подобном ЯП
entry получить (X : out сообщение);
end буф; -- ключевое слова "monitor" и "entry" подчеркивают
-- особую семантику процедур "поставить" и
-- "получить", т.е. режим взаимного исключения
monitor body буф is
procedure поставить (X: in сообщение) is
begin
if полон then ждать (неполон) end if;
занести (X); -- "обычная" запись в буфер
послать (непуст); -- сигнал для "получить"
end поставить;
procedure получить (X: out сообщение) is
begin
if пуст then ждать (непуст) end if;
выбрать (X); -- "обычная" выборка из буфера
послать (неполон); -- сигнал для "поставить"
end получить;
end буф;
Чтобы все встало на свои места, в пакете "общий" нужно названия
процедур "поставить" и "получить" заменить на "занести" и "выбрать"
соответственно. Процедуры, объявленные в мониторе ("мониторные" или
"монопольные" процедуры) пользуются "обычными" пакетными процедурами, в
которых можно конкретизировать такие особенности буфера, как его организацию
массивом или списком, очередность выборки сообщений и т.п., не относящиеся к
параллелизму.
Важно понимать, что сам по себе режим взаимного исключения не спасает
от тупиков. Ведь, например, при переполнении буфера процедура "поставить" не
может нормально завершить свою работу, и если не внести уточнений в
семантику монитора, то нельзя избежать тупика (пока не завершена процедура
"поставить", не может работать "получить", чтобы освободить буфер!).
Поэтому на самом деле мониторные процедуры могут быть приостановлены в
указанных программистом местах с тем, чтобы дать возможность запускать
другие процедуры. Для этого можно воспользоваться аппаратом сигналов, что и
сделано в нашем примере. Так что, например, первая процедура может
приостановиться на операторе "ждать(неполон)", и так как в мониторе не
остается активных процедур, он готов при необходимости активизировать вторую
процедуру (которая в этом случае наверняка пошлет сигнал "неполон"), а после
завершения второй процедуры сможет продолжить свою работу первая.
Итак, в семантику сигналов также внесена "мониторная" коррекция: можно
возобновлять процесс из очереди только при условии, что он не приостановлен
на процедуре из активного монитора.
[Полезно подчеркнуть аналогию между взаимодействием мониторных процедур
и сопрограмм.
Напомним, что такое процессы-сопрограммы Х и Y:
process X; process Y;
. . . . . .
resume Y; resume X;
. . . . . .
resume Y; resume X;
. . . . . .
detach; detach; ==> главная программа
Процессы-сопрограммы исполняются на одном процессоре. Оператор resume
приостанавливает исполнение сопрограммы, в которой находится, и возобновляет
исполнение указанной в нем сопрограммы с того места, на котором она была
ранее приостановлена (или с самого начала, если это первое обращение к ней).
Оператор detach возвращает управление главной программе.
Как и в случае сопрограмм, для каждого монитора предполагается
единственный исполнитель, способный приостанавливать исполнение мониторных
процедур (на операторе "ждать") и возобновлять их исполнение с прерванного
места. Однако в отличие от сопрограмм приостановка не означает немедленное