Основы многопоточного и распределенного программирования

       

Нотация совместно используемых примитивов


При использовании RPC и рандеву процесс инициирует взаимодействие, выполняя опе­ратор call, который блокирует вызвавший процесс до того, как вызов будет обслужен и ре­зультаты возвращены. Такая последовательность действий идеальна для программирования взаимодействий типа "клиент-сервер", но, как видно из двух последних разделов, усложняет программирование фильтров и взаимодействующих равных. С другой стороны, односторон­ний поток информации между фильтрами и равными процессами легче программировать с помощью асинхронной передачи сообщений.

В данном разделе описана программная нотация, которая объединяет RPC, рандеву и асинхронную передачу сообщений в единое целое. Такая составная нотация (нотация со­вместно используемых примитивов) сочетает преимущества всех трех ее компонентов и обес­печивает дополнительные возможности.

8.3.1. Вызов и обслуживание операций

По своей структуре программа будет набором модулей. Видимые операции объявляют­ся в области определений модуля. Эти операции могут вызываться процессами из других модулей, а обслуживаются процессом или процедурой модуля, в котором объявлены. Мо­гут использоваться также локальные операции, которые объявляются, вызываются и обслу-•• живаются в теле одного модуля.

В составной нотации операция может быть вызвана либо синхронным оператором call, либо асинхронным send. Они имеют следующий вид.

'                        call Mname. opname (аргументы) ;

send Mname.opname (аргументы);

Оператор call завершается, когда операция обслужена и возвращены результирующие аргу­менты, а оператор send — как только вычислены аргументы. Если операция возвращает резуль­тат, ее можно вызывать в выражении; ключевое слово call опускается. Если операция имеет результирующие параметры и вызывается оператором send, или функция вызывается оператором send, или вызов функции находится не в выражении, то возвращаемое значение игнорируется.

В составной нотации операция может быть обслужена либо в процедуре (ргос), либо с помощью рандеву (операторы in).
Выбор — за программистом, который объявляет опера­цию в модуле. Это зависит от того, нужен ли программисту новый процесс для обслуживания вызова, или удобнее использовать рандеву с существующим процессом. Преимущества и не­достатки каждого способа демонстрируются в дальнейших примерах.

Когда операцию обслуживает процедура (ргос), для обработки вызова создается новый ' процесс. Вызов операции даст тот же результат, что и при использовании RPC. Если опера­ция вызвана оператором send, результат будет тем же, что и при создании нового процесса, поскольку вызвавший операцию процесс продолжается асинхронно по отношению к процес­су, обслуживающему вызов. В обоих случаях вызов обслуживается немедленно, и очереди ожидающих обработки вызовов нет.

Другой способ обслуживания операций состоит в использовании операторов ввода, которые имеют вид, указанный в разделе 8.2. С каждой операцией связана очередь ожидающих обработ­ки вызовов, и доступ к этой очереди является неделимым. Выбор операции для обслуживания происходит в соответствии с семантикой операторов ввода. При вызове такой операции процесс приостанавливается, поэтому результат аналогичен использованию рандеву. Если такую опера­цию вызвать с помощью оператора send, результат будет аналогичным использованию асин­хронной передачи сообщений, поскольку отправитель сообщения продолжает работу.

Итак, есть два способа вызова операции (операторы call и send) и два способа обслужи­вания вызова — ргос и in. Эти четыре комбинации приводят к таким результатам.

Глава 8 Удаленный вызов процедур и рандеву                                                                  299

Вызов                     Обслуживание                 Результат



call                          proc                                       Вызов процедуры

call                       in                                     Рандеву

send                             proc                                           Динамическое создание процесса



send                             in                                              Асинхронная передача сообщения

Если вызывающий процесс и процедура proc находятся в одном модуле, то вызов является ло­кальным, иначе — удаленным. Операцию нельзя обслуживать как с помощью proc, так и в опера­торе in, поскольку тогда возникает неопределенность — обслужить операцию немедленно или поместить в очередь. Но операцию можно обслуживать в нескольких операторах ввода; они могут находиться в нескольких процессах модуля, в котором объявлена операция. В этом случае про­цессы совместно используют очередь ожидающих вызовов, но доступ к ней является неделимым.

Для мониторов и асинхронной передачи сообщений был определен примитив empty, ко­торый проверяет, есть ли объекты в канале сообщений или очереди условной переменной. В этой главе будет использован аналогичный, но несколько отличающийся примитив. Если opname является операцией, то ?opname — это функция, которая возвращает число ожи­дающих вызовов этой операции. Эту функцию удобно использовать в операторах ввода. На­пример, в следующем фрагменте кода операция ор1 имеет приоритет перед операцией ор2.

in   opl(...)    ->   SI;

[]   ор2(...)   and  ?opl  ==  0  ->  S2;

_        

Условие синхронизации во второй защите разрешает выбор операции ор2, только если при вычислении ?opl определено, что вызовов операции opl нет.

8.3.2. Примеры

Различные способы вызова и обслуживания операций проиллюстрируем тремя неболь­шими, тесно связанными примерами. Вначале рассмотрим реализацию очереди (листинг 8.11). Когда вызывается операция deposit, Bbuf помещается новый элемент. Если deposit вызвана оператором call, то вызывающий процесс ждет; если deposit вызвана оператором send, то вызывающий процесс продолжает работу (в этом случае процессу, вы­зывающему операцию, возможно, стоило бы убедиться, не переполнен ли буфер). Когда вы­зывается операция fetch, из массива buf извлекается элемент. Ее необходимо вызывать оператором call, иначе вызывающий процесс не получит результат.



Модуль Queue пригоден для использования одним процессом в другом модуле. Его не могут совместно использовать несколько процессов, поскольку в модуле нет критических секций для защиты переменных модуля. При параллельном вызове операций может возник­нуть взаимное влияние.

Если нужна синхронизированная очередь, модуль Queue можно изменить так, чтобы он реализовывал кольцевой буфер. В листинге 8.5 был представлен именно такой модуль. Види­мые операции в этом модуле те же, что и в модуле Queue. Но их вызовы обслуживаются опе­ратором ввода в одном процессе, т.е. по одному. Операция fetch должна вызываться опера­тором call, однако для операции deposit вполне можно использовать оператор send.

Модули в листингах 8.5 и 8.11 демонстрируют два разных способа реализации одного итого же интерфейса. Выбор определяется программистом и зависит от того, как именно ис­пользуется очередь. Но есть еще один способ реализации кольцевого буфера, иллюстрирую­щий еще одно сочетание различных способов вызова и обслуживания операций в нотации совместно используемых примитивов.



Поскольку оператор receive является просто сокращенной формой оператора in, будем использовать receive, когда нужно обработать вызов именно таким образом.

Теперь рассмотрим операцию, которая не имеет аргументов, вызывается оператором send и обслуживается оператором receive (или эквивалентным in). Такая операция эквива­лентна семафору, причем send выступает в качестве V, a receive — Р. Начальное значение семафора равно нулю. Его текущее значение — это число "пустых" сообщений, переданных операции, минус число полученных сообщений.

В листинге 8.12 представлена еще одна реализация модуля BoundedBuf f er, в которой для синхронизации использованы семафоры. Операции deposit и fetch обслуживаются проце­дурами так же, как в листинге 8.11. Следовательно, одновременно может существовать несколь­ко активных экземпляров этих процедур. Однако для реализации взаимного исключения и ус­ловной синхронизации в этой программе используются семафорные операции, как в листинге 4.5.


Структура этого модуля аналогична структуре монитора (см. листинг 5.3), но синхронизация реа­лизована с помощью семафоров, а не исключений монитора и условных переменных.





Две реализации кольцевого буфера (см. листинги 8.5 и 8.11) иллюстрируют важную взаимо­связь между условиями синхронизации в операторах ввода и явной синхронизацией в процеду­рах. Во-первых, их часто используют с одинаковой целью. Во-вторых, поскольку условия синхро­низации операторов ввода могут зависеть от аргументов ожидающих вызовов, эти два метода син­хронизации обладают равной мощью. Но пока не нужна параллельность, которую обеспечивают несколько вызовов процедур, эффективнее использовать рандеву клиентов с одним процессом.


Содержание раздела