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

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

Введение параллельного выполнения

Что же, если не понятие процесса, фундаментально отличает параллельное вычисление от последовательного?

Процессоры

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

Три силы вычисления

Рис. 12.2. Три силы вычисления

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

А что же процессоры? Разумеется, нам нужен механизм для выполнения действий над объектами. Но последовательное вычисление образует лишь одну ветвь управления, для которой нужен лишь один процессор, большую часть времени присутствовавший в предыдущих лекциях неявно.

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

Природа процессоров

Определение: процессор

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

Это абстрактное понятие, его не надо путать с физическими устройствами, называемыми процессорами, для которых мы далее будем использовать термин ЦПУ ( CPU ), обычно используемый в компьютерной инженерии для обозначения процессорных единиц компьютеров. "ЦПУ" - это сокращение для названия "Центральное процессорное устройство", хотя почти ничего центрального в ЦПУ нет. ЦПУ можно использовать для реализации процессора, но понятие процессора существенно более общее и абстрактное. Например, процессор может быть:

  • компьютером (со своим ЦПУ) в сети;
  • заданием, также называемым процессом, - поддерживается такими операционными системами, как Unix, Windows и многими другими;
  • сопрограммой (сопрограммы будут более детально рассмотрены далее, они моделируют реальную параллельность, выполняясь по очереди на одном ЦПУ, после каждого прерывания каждая сопрограмма продолжает выполнение с того места, где оно остановилось);
  • "потоком", который поддерживается в таких многопоточных операционных системах как Solaris, OS/2 и Windows NT.

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

Различие между процессорами и ЦПУ было ясно описано Генри Либерманом ([Lieberman 1987])(для другой модели параллельности):

Не нужно ограничивать заранее число [процессоров] и, если их оказывается больше, чем имеется реальных физических [ЦПУ] у вашего компьютера, то они автоматически будут разделять время. Таким образом, пользователь может считать, что ресурс процессоров у него практически бесконечен.

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

Раньше или позже потребуется назначать вычислительные ресурсы процессорам. Это отображение будет представлено с помощью "файла управления параллелизмом" ("Concurrency Control File"), описываемого ниже, или соответствующих библиотечных средств.

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

Каждый компонент должен быть обработан (выполнен) некоторым процессором. Вообще, каждый объект O2 обрабатывается некоторым процессором - его обработчиком, обработчик ответственен за выполнение всех вызовов компонентов O2 (т. е. всех вызовов вида x.f (a), где x присоединен к O2).

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

  • позволив переназначать процессору выполняющее его ЦПУ (при таком подходе все объекты, обрабатываемые некоторым процессором, будут мигрировать вместе);
  • трактуя миграцию объекта как создание нового объекта.

Дуальная семантика вызовов

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

x.f (a)         -- если f - команда
y := x.f (a)    -- если f - запрос

Пусть, как и раньше, O2 - объект, присоединенный в момент вызова к x, а O1 - объект, от имени которого выполняется вызов. (Иными словами, команда любого указанного вида является частью подпрограммы, имеющей цель O1).

Мы привыкли понимать действие вызова как выполнение тела f, примененного к O2 с использованием a в качестве аргумента и возвратом некоторого результата в случае запроса. Если такой вызов является частью последовательности инструкций:

... previous_instruction; x.f (a); next_instruction; ...

(или ее эквивалента в случае запроса), то выполнение next_instruction не начнется до того, как завершится вызов f.

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

Поэтому вместо одной семантики вызова у нас появляются две:

  • Если у O1 и O2 один и тот же обработчик, то всякая следующая операция O1 (next_instruction) должна ждать завершения вызова. Такие вызовы называются синхронными.
  • Если O1 и O2 обрабатываются разными процессорами, то операции O1 могут продолжаться сразу после того, как он инициирует вызов O2. Такие вызовы называются асинхронными.

Асинхронный случай особенно интересен для выполнения команды, так как результаты вызова O2 могут вовсе не понадобиться или понадобиться оставшейся ее части гораздо позже. О1 может просто отвечать за запуск одного или нескольких параллельных вычислений и за их завершение. В случае запроса результат, конечно, нужен, например, выше его значение присваивается y, но ниже будет объяснено, как можно продолжать параллельную работу и в этом случае.

Сепаратные сущности

Общее правило разработки ПО заключается в том, что семантическое различие всегда должно отражаться на различии текстов программ.

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

Это рассуждение приводит к тому, что для поддержки параллельности достаточно одного расширения нотации. Наряду с обычным объявлением:

x: SOME_TYPE

мы будем использовать объявление вида:

x: separate SOME_TYPE

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

separate class X ... Остальное как обычно ...
вместо обычных объявлений class X ... или deferred class X ...

Это соглашение аналогично тому, что можно объявить y как сущность типа expanded T или, что эквивалентно, как сущность типа T, если T - это класс, объявленный как expanded class T. Три возможности - развернутый (expanded), отложенный (deferred), сепаратный (separate) - являются взаимно исключающими, только одно из этих квалифицирующих слов может стоять перед словом class.

Это даже поразительно, что достаточно добавить одно ключевое слово для превращения последовательной ОО-нотации в систему обозначений, поддерживающую параллельные вычисления.

Уточним терминологию. Слово "сепаратный" ("separate") можно применять к различным элементам, как статическим (появляющимся в тексте программы), так и динамическим (существующим во время выполнения). Статически: сепаратный класс - это класс, объявленный как separate class ; сепаратный тип основывается на сепаратном классе; сепаратная сущность это сущность сепаратного типа или сущность, объявленная как separate T для некоторого T ; x.f (...) - это сепаратный вызов, если его цель x является сепаратной сущностью. Динамически: значение сепаратной сущности является сепаратной ссылкой; если она не пуста, то присоединяется к объекту, обрабатываемому отдельным процессором - сепаратному объекту.

Типичными примерами сепаратных классов являются:

  • BOUNDED_BUFFER ( ОГРАНИЧЕННЫЙ_БУФЕР ) задает буфер, позволяющий параллельным компонентам обмениваться данными (некоторые компоненты - производители - помещают объекты в буфер, а другие - потребители - получают объекты из него).
  • PRINTER ( ПРИНТЕР ), который, по-видимому, правильней называть PRINT_CONTROLLER ( КОНТРОЛЕР_ПЕЧАТИ ), управляет одним или несколькими принтерами. Считая контроллеры печати сепаратными объектами, приложения не должны ждать завершения заданий на печать (в отличие от ранних Макинтошей, в которых вы застревали до тех пор, пока последняя страница не выползала из принтера).
  • DATABASE ( БАЗА ДАННЫХ ), клиентская часть которой в архитектуре клиент-сервер может служить для описания базы данных, расположенной на удаленном сервере, которому клиент может посылать запросы по сети.
  • BROWSER_WINDOW ( ОКНО_БРАУЗЕРА ) позволяет порождать новое окно для просмотра запрошенной страницы.

Получение сепаратных объектов

Как показывают предыдущие примеры, на практике встречаются сепаратные объекты двух видов:

  • В первом случае приложение при вызове захочет порождать новый сепаратный объект, заняв следующий свободный процессор. (Напомним, что такой процессор всегда можно получить, так как процессоры - это не материальные ресурсы, а абстрактные устройства, и их число не ограничено). Эта ситуация типична для BROWSER_WINDOW: новое окно создается тогда, когда это нужно. Объекты классов BOUNDED_BUFFER или PRINT_CONTROLLER также могут создаваться при необходимости.
  • Приложению может потребоваться доступ к уже существующему сепаратному объекту, обычно разделяемому многими клиентами. Это имеет место для класса DATABASE: приложение-клиент использует сепаратную сущность db_server: separate DATABASE для доступа к базе данных через сепаратные вызовы вида db_server.ask_query (sql_query). У сервера должно быть полученное на некотором шаге извне значение указателя на базу данных server. Аналогичные схемы используются для доступа к существующим объектам классов BOUNDED_BUFFER и PRINT_CONTROLLER.

Скажем, что в первом случае сепаратные объекты создаются, а во втором являются внешними.

Для создания сепаратного объекта применяется обычная инструкция создания:

create x.make (...)

В дополнение к своему обычному действию по созданию и инициализации нового объекта ему назначается новый процессор. Такая инструкция называется сепаратным созданием:

Для получения существующего внешнего объекта, как правило, используется внешняя процедура, например:

server (name: STRING; ... Другие аргументы ...): separate DATABASE

Ее аргументы служат для идентификации запрашиваемого объекта. Такая процедура посылает сообщение по сети и получает в ответ ссылку на объект.

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

Прокси для сепаратного объекта

Рис. 12.3. Прокси для сепаратного объекта

На этом рисунке показан объект O1, экземпляр класса T с атрибутом x: separate U. Соответствующее поле - ссылка в O1 - концептуально привязано к объекту O2, обрабатываемому другим процессором. Фактически ссылка ведет к прокси-объекту, обрабатываемому процессором для O1. Прокси - это внутренний объект, не видимый автору параллельного приложения. Он содержит достаточно информации для идентификации O2: задачу, которая служит обработчиком O2, и адрес O2 внутри этой задачи. Все операции над x, проводимые от имени O1 или других клиентов той же задачи, будут проходить через прокси. У всякого другого процессора, также обрабатывающего объекты, содержащие ссылки на O2, будет свой собственный прокси для O2.

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

Объекты здесь и там

Некоторые люди, впервые познакомившись с понятием сепаратной сущности, выражают недовольство тем, что оно чересчур детализировано: "Я не хочу знать, где расположен объект! Я хотел бы лишь запрашивать операцию x.f (...), а остальное пусть делает машинерия - выполняет f на x, где бы x не находился".

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

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