Опубликован: 23.10.2005 | Доступ: свободный | Студентов: 3868 / 144 | Оценка: 4.44 / 4.19 | Длительность: 33:04:00
Специальности: Программист
Лекция 12:

Параллельность, распределенность, клиент-сервер и Интернет

От процессов к объектам

Для поддержки этих захватывающих дух достижений, требующих параллельной обработки, нужна мощная программная поддержка. Как мы собираемся программировать эти вещи? Конечно, для этого предлагается ОО-технология.

Говорят, что Робин Мильнер (Robin Milner) воскликнул в 1991 на одном из семинаров ОО-конференции: "Я не могу понять, почему параллельность объектов [ОО-языков] не стоит на первом месте" (цитируется по [Matsuoka 1993]). Даже, если поставить ее на второе или на третье место, то остается вопрос, как придти к созданию параллельных объектов?

Если рассмотреть параллельную работу не в ОО-контексте, то она в большой степени основана на понятии процесса. Процесс - программная единица - действует как специализированный компьютер: он выполняет некоторый алгоритм, как правило, многократно, пока некоторое внешнее событие не приведет к его завершению. Типичным примером является процесс управления принтером, который последовательно повторяет:

"Ждать появления задания в очереди на печать"
"Взять задание и удалить его из очереди"
"Напечатать задание"

Разные модели параллельности различаются планированием и синхронизацией, борьбой за ресурсы, обменом информацией. В одних языках параллельного программирования непосредственно описываются процессы, в других, таких как Ada, можно также описывать типы процессов, которые во время выполнения реализуются в процессах так же, как классы ОО-ПО реализуются в объектах.

Сходство

Это соответствие кажется очевидным. Когда мы начинаем сравнивать идеи параллельного программирования и ОО-построения программ, то кажется естественным идентифицировать процессы с объектами, а типы процессов с классами. Каждый, кто вначале изучил параллельные вычисления, а затем открыл ОО-разработку (или наоборот) будет удивлен сходством между этими двумя технологиями:

  • Обе основаны на автономных, инкапсулированных модулях: процессах или типах процессов и на классах.
  • Объекты и процессы сохраняют содержащиеся в них значения от одной активации до следующей.
  • Для построения параллельной системы на практике требуется налагать строгие ограничения на межмодульный обмен информацией. ОО-подход, как мы видели, тоже налагает строгие ограничения на межмодульную коммуникацию.
  • В обоих случаях механизм коммуникации можно упрощенно описать как "передачу сообщений".

Поэтому неудивительно, что многие люди восклицают "Эврика!", когда впервые начинают размышлять, подобно Мильнеру, о наделении объектов параллельностью. Кажется, что можно легко достичь унификации этих понятий.

К сожалению, это первое впечатление ошибочно: после обнаружения первого сходства быстро сталкиваешься с различиями.

Активные объекты

Основываясь на приведенных выше аналогиях, в многочисленных предложениях параллельных ОО-механизмов было введено понятие "активного объекта". Активный объект - это объект, являющийся также процессом: у него есть собственная исполняемая программа. Вот как он определяется в одной книге по языку Java [Doug Lea 1996]:

Каждый объект является единой идентифицируемой процессоподобной сущностью (не отличающейся (?) от процесса в Unix) со своим состоянием и поведением.

Однако это понятие приводит к тяжелым проблемам. Легко понять самую важную из них. У процесса имеется собственный план решения задачи: на примере с принтером видно, что он постоянно выполняет некоторую последовательность действий. А у классов и объектов дело обстоит не так. Объект не делает одно и то же, он является хранилищем услуг (компонентов порожденного класса) и просто ожидает, когда очередной клиент запросит одну из этих услуг - она выбирается клиентом, а не объектом. Если сделать объект активным, то он сам станет определять расписание выполнения своих операций. Это приведет к конфликту с клиентами, которые совершенно точно знают, каким должно быть это расписание: им нужно только, чтобы поставщик в любой момент, когда от него потребуется конкретная услуга, был готов немедленно ее предоставить!

Эта проблема возникает и в не ОО-подходах к параллельности и приводит к механизмам синхронизации процессов - иначе говоря, к определению того, когда и как каждый процесс готов общаться, ожидая, если требуется, готовности другого процесса. Например, в очень простой схеме производитель-потребитель (producer-consumer) может быть процесс producer, который последовательно повторяет следующие действия:

"Сообщает, что producer не готов"
"Выполняет вычисление значения x"
"Сообщает, что producer готов"
"Ожидает готовности consumer"
"Передает x consumer"

и процесс consumer, который последовательно повторяет

"Сообщает, что consumer готов"
"Ожидает готовности producer"
"Получает x от producer"
"Сообщает, что consumer не готов"
"Выполняет вычисление, использующее значение x"

Графически эту схему можно представить так

Простая схема производитель-потребитель (producer-consumer)

Рис. 12.1. Простая схема производитель-потребитель (producer-consumer)

Общение процессов происходит, когда оба они к этому готовы; это иногда называется handshake (рукопожатие) или rendez-vous (рандеву). Проектирование механизмов синхронизации - позволяющих точно выражать смысл команд "Известить о готовности процесса" или "Ждать готовности" - на протяжении нескольких десятилетий является плодотворной областью исследований.

Все это хорошо для процессов, являющихся параллельными эквивалентами традиционных последовательных программ, "делающих одну вещь". Параллельная система, построенная на процессах, похожа на последовательную систему с несколькими главными программами. Но при ОО-подходе мы уже отвергли понятие главной программы и вместо нее определили программные единицы, предоставляющие клиентам несколько компонентов.

Согласование этого подхода с понятием процесса требует тщательно проработанных конструкций синхронизации, когда каждый поставщик будет готов выполнить компонент, затребованный клиентом. Согласование должно быть особенно тщательным, когда клиент и поставщик являются активными объектами, придерживающихся своих порядков действий.

Изобретение механизмов, основанных на понятии активного объекта, вполне возможно, о чем свидетельствует обильная литература по этому предмету (в библиографических заметках к этой лекции приведено много соответствующих ссылок). Но это обилие свидетельствует о сложности предлагаемых решений, ни одно из которых не получило широкого признания, из чего можно заключить, что подход с использованием активных объектов сомнителен.

Конфликт активных объектов и наследования

Еще большие сомнения в правильности подхода, использующего активные объекты, появляются при его объединении с другими ОО-механизмами, особенно, с наследованием.

Если класс B является наследником класса A и оба они активны (т. е. описывают экземпляры, которые должны быть активными объектами), то что произойдет в B с описанием процесса A? Во многих случаях потребуется добавлять некоторые новые инструкции, но без специальных встроенных в язык механизмов это повлечет необходимость почти всегда переопределять и переписывать всю часть, относящуюся к процессу, - не очень привлекательное предложение.

Приведем пример одного специального языкового механизма. Хотя язык Simula 67 не поддерживает параллельность, в нем есть понятие активного объекта: класс Simula помимо компонентов содержит инструкции, называемые телом класса (см. "От Simula к Java и далее: основные ОО-языки и окружения" ). В теле класса A может содержаться специальная инструкция inner, не влияющая на сам класс, означающая подстановку собственного тела в потомке B. Так что, если тело A имеет вид:

some_initialization; inner; some_termination_actions

а тело B имеет вид:

specific_B_actions

то выполнение тела в B на самом деле означает:

some_initialization; specific_B_actions; some_termination_actions

Хотя необходимость механизмов такого рода для языков, поддерживающих понятие активного объекта, не вызывает сомнений, на ум сразу приходят возражения. Во-первых, эта нотация вводит в заблуждение, поскольку, зная только тело B, можно получить неверное представление его выполнения. Во-вторых, это заставляет родителя предугадывать действия наследников, что в корне противоречит основополагающим принципам ОО-проектирования (в частности принципу Открыт-Закрыт) и годится только для языка с единичным наследованием.

Основная проблема останется и при другой нотации: как соединить спецификацию процесса в классе со спецификациями процессов в его потомках, как примирить спецификации процессов нескольких родителей в случае множественного наследования?

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

Встретившись с этими трудностями, некоторые из ранних предложений по ОО-параллельности предпочли вообще отказаться от наследования. Хотя это оправдано, как временная мера, призванная помочь пониманию предмета путем разделения интересов, такое исключение наследования не может оставаться при окончательном выборе подхода к построению параллельного ОО-ПО; это было бы похоже на желание отрезать руку из-за того, что чешутся пальцы. (Для оправдания в некоторых источниках добавляют, что все равно наследование - это сложное и темное понятие, это - как сказать пациенту после операции, что иметь руку с самого начала было плохой идеей.)

Вывод, к которому мы можем придти, проще. Проблема не в ОО-технологии как таковой, и в частности, не в наследовании; она не в параллельности и даже не в комбинации этих идей. Источником неприятностей является понятие активного объекта.

Программируемые процессы

Поскольку мы готовы избавиться от активных объектов, полезно заметить, что на самом деле мы не хотим ни от чего отказываться. Объект способен выполнять много операций: все компоненты породившего его класса. Превращая объект в процесс, приходится выбирать одну из этих операций в качестве единственной реально вычисляемой. Это не дает абсолютно никаких преимуществ! Зачем ограничивать себя одним алгоритмом, когда можно иметь их столько, сколько нужно?

Заметим, что понятие процесса не обязательно должно быть встроено внутрь механизма параллельности; процессы можно программировать, рассматривая их как обычные программы. Процесс для принтера, приведенный в начале лекции, с ОО-точки зрения может трактоваться как одна из подпрограмм, скажем, live, соответствующего класса:

indexing
  description: "Принтер, выполняющий в каждый момент одно задание"
  note: "Улучшеная версия, основанная на общем классе PROCESS, %
      %появится далее под именем PRINTER"
class
  PRINTER_1
feature -- Status report
  stop_requested: BOOLEAN is do ... end
  oldest: JOB is do ... end
feature -- Basic operations
  setup is do ... end
  wait_for_job is do ... end
  remove_oldest is do ... end
  print (j: JOB) is do ... end
feature -- Process behavior
  live is
      -- Выполнение работы принтера
    do
      from setup until stop_requested loop
        wait_for_job; print (oldest); remove_oldest
      end
    end
  ... Другие компоненты ...
end

Отметим заготовку для других компонентов: хотя до сих пор все наше внимание было уделено live и окружающим его компонентам, мы можем снабдить процесс и многими другими желательными компонентами, чему способствует ОО-подход, развитый в других частях этого курса. Превращение объектов класса PRINTER_1 в процессы означало бы ограничение этой свободы, это была бы существенная потеря в выразительной силе без всякой видимой компенсации.

Абстрагируясь от этого примера, который описывает конкретный тип процесса просто как некоторый класс, можем попытаться предложить более общее описание всех типов процессов с помощью специального отложенного класса - класса поведения, как это уже не раз делалось в предыдущих лекциях. Процедура live будет применима ко всем процессам. Мы можем оставить ее отложенной, но нетрудно заметить, что большинство процессов будут нуждаться в некоторой инициализации, некотором завершении, а между ними - в некотором основном шаге, повторяемом некоторое число раз. Поэтому мы можем учесть это на самом абстрактном уровне:

indexing
  description: "Самое общее понятие процесса"
deferred class
  PROCESS
feature -- Status report
  over: BOOLEAN is
      -- Нужно ли сейчас прекратить выполнение?
    deferred
    end
feature -- Basic operatios
  setup is
-- Подготовка к выполнению операций процесса
-- (по умолчанию: ничего)
    do
    end
  step is
      -- Выполнение основных операций
    deferred
    end
  wrapup is
-- Выполнение операций завершения процесса
-- (по умолчанию: ничего)
    do
    end
feature -- Process behavior
  live is
      -- Выполнение жизненного цикла процесса
    do
      from setup until over loop
        step
      end
      wrapup
    end
end

Методологическое замечание: компонент step является отложенным, но setup и wrapup являются эффективными процедурами, которые по определению ничего не делают. Так можно заставить каждого эффективного потомка обеспечить собственную реализацию основного действия процесса step, не беспокоясь об инициализации и завершении, если на этих этапах не требуется специальных действий. При проектировании отложенных классов выбор между отложенной версией и пустой эффективной версией приходится делать регулярно. Ошибки не страшны, поскольку в худшем случае потребуется выполнить больше работы по эффективизации или переопределению у потомков.

Используя данный образец, можно определить специальный класс, охватывающий принтеры:

indexing
  description: "Принтеры, выполняющие в каждый момент одно задание"
  note: "Пересмотренная версия, основанная на классе PROCESS"
class PRINTER inherit
  PROCESS
    rename over as stop_requested end
feature -- Status report
  stop_requested: BOOLEAN
      -- Является ли следующее задание в очереди запросом на
      -- завершение работы?
  oldest: JOB is
      -- Первое задание в очереди
    do ... end
feature -- Basic operations
  step is
      -- Обработка одного задания
    do
      wait_for_job; print (oldest); remove_oldest
    end
  wait_for_job is
      -- Ждать появления заданий в очереди
    do
      ...
    ensure
      oldest /= Void
    end
  remove_oldest is
      -- Удалить первое задание из очереди
    require
      oldest /= Void
    do
      if oldest.is_stop_request then stop_requested := True end
      "Удалить первое задание из очереди"
    end
  print (j: JOB) is
      -- Печатать j, если это не запрос на остановку
    require
      j /= Void
    do
      if not j.is_stop_request then "Печатать текст, связанный с j"
      end
    end
end

Этот класс предполагает, что запрос на остановку принтера посылается как специальное задание на печать j, для которого выполнено условие jlis_stop_request. (Было бы лучше устранить проверку условия в print и remove_oldest, введя специальный вид задания - "запрос на остановку"; это нетрудно сделать [см. У12.1]).

Уже сейчас видны преимущества ОО-подхода. Точно так же, как переход от главной программы к классам расширил наши возможности, предоставив абстрактные объекты, не ограничивающиеся "только одним делом", рассмотрение процесса принтера как объекта, описанного некоторым классом, открывает возможность новых полезных свойств. В случае принтера можно сделать больше, чем просто выполнять обычную операцию печати, обеспечиваемую live (которую нам, возможно, придется переименовать в operate, при наследовании ее из PROCESS ).

Можно добавить компоненты: perform_internal_test ( выполнить внутренний тест ), switch_to_Postscript_level_1 ( переключиться на уровень Postscript1 ) или set_resolution ( установить разрешение ). Стабилизирующее влияние ОО-метода здесь так же важно, как и для последовательного ПО.

Классы, очерченные в этом разделе, показывают, как можно применять нормальные ОО-механизмы - классы, наследование, отложенные элементы, частично реализованные образцы - к реализации процессов. Нет ничего плохого в понятии процесса в контексте ОО-программирования, и на самом деле оно требуется во многих параллельных приложениях. Но, вместо включения некоторого примитивного механизма, оно просто охватывается библиотечным классом PROCESS, основанным на версии, приведенной выше в этом разделе или, может быть, несколькими такими классами, охватывающими различные варианты этого понятия.

Что касается новой конструкции для параллельной ОО-технологии, то она будет рассмотрена далее.

Александр Качанов
Александр Качанов
Япония, Токио
Янош Орос
Янош Орос
Украина, Киев