Рекомендация: Параллелизм
Настоящая рекомендация поможет разработчикам выбрать наилучший способ поддержки параллелизма в системе программного обеспечения.
Взаимосвязи
Основное описание

Введение

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

Подходы к обеспечению параллелизма

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

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

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

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

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

Поддержка параллелизма в системе может оказать значительное влияние на ее архитектуру. Решение о переходе от однопроцессной к многопроцессной архитектуре предполагает серьезные изменения в структуре системы, причем во многих аспектах. Может потребоваться введение дополнительных механизмов (например, вызовов удаленных процедур), которые могут сильно отразиться на архитектуре системы.

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

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

Подход

Преимущества

Недостатки

Однопроцессность, отсутствие нитей
  • Простота
  • Быстрый внутрипроцессный обмен сообщениями
  • Трудности с выравниванием нагрузки
  • Невозможность добавления процессоров
Однопроцессность, поддержка нескольких нитей
  • Быстрый внутрипроцессный обмен сообщениями
  • Многозадачность без межпроцессной связи
  • Лучшая многозадачность без нагрузки, связанной со 'сложными' процессами
  • Необходимость защиты нитей в приложении
  • Необходимость эффективного управления нитями в операционной системе
  • Необходимость учета совместного использования памяти
Многопроцессность
  • Простота масштабирования при добавлении процессоров
  • Сравнительная легкость в распределении по узлам
  • Чувствительность к границам процессов: межпроцессная связь слишком сильно снижает производительность
  • Дорогостоящие подкачка и переключение контекста
  • Сложности в проектировании

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

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

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

Вопросы

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

  • Можно ли повысить число процессов до максимума? Насколько можно превысить это число сверх максимума? Есть ли возможности для роста?
  • Какое влияние окажет превращение некоторых процессов в упрощенные нити, работающие в совместно используемом адресном пространстве процесса?
  • Что происходит с временем ответа при увеличении числа процессов? Увеличивается ли интенсивность межпроцессной связи (IPC)? Насколько заметно снижение производительности?
  • Можно ли снизить IPC путем комбинирования или реорганизации процессов? Приведет ли такое изменение к появлению больших монолитных процессов, в которых трудно выравнивать нагрузку?
  • Можно ли снизить IPC за счет применения совместно используемой памяти?
  • Следует ли равномерно распределять ресурсы времени между всеми процессами? Возможно ли управлять распределением ресурсов времени? Существуют ли потенциальные препятствия для изменения приоритетов планирования?

Межобъектная связь

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

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

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

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

В подходе, рекомендованном в Rational Unified Process для систем, работающих в режиме реального времени, капсулы контактируют асинхронно посредством сигналов, согласно конкретным протоколам. Тем не менее, можно осуществлять и синхронную связь - за счет использования пар сигналов, по одному в каждом направлении.

Прагматические соображения

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

Указанные соображения, однако, не обязательно означают, что мы должны вовсе отказаться от применения активных объектов. Даже в приложениях, интенсивно обменивающихся данными, часто бывает так, что компоненты, чувствительные к производительности, являются лишь небольшой частью всей системы. Это означает, что в остальной части системы можно пользоваться активными объектами.

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

Эвристические соображения

Сконцентрируйтесь на взаимодействии параллельных компонентов

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

  • Является ли взаимодействие одно-, дву- или мультинаправленным?
  • Существует ли взаимосвязь типа клиент-сервер или типа главный-подчиненный?
  • Требуется ли синхронизация в какой-либо форме?

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

Эти вопросы еще более актуальны для систем, работающих в режиме реального времени. Такие системы обычно очень чувствительны к колебаниям производительности, например 'искажениям' во времени ответа или нарушению сроков выполнения требуемых действий.

Изолируйте и инкапсулируйте внешние интерфейсы

Весьма опасно строить работу приложения на основе каких-либо конкретных предположений о внешних интерфейсах и крайне неэффективно блокировать сразу несколько управляющих нитей в ожидании события. Вместо этого назначьте отдельному объекту выделенную задачу обнаружения события. Когда событие произойдет, этот объект сможет уведомить об этом всех, кому это необходимо. Такая конфигурация основана на стандартном и хорошо себя зарекомендовавшем шаблоне проектирования "Наблюдатель" [GAM94]. При необходимости ее легко расширить до более гибкого шаблона "Издатель-подписчик", в котором издатель играет роль промежуточного объекта между объектами, обнаруживающими события, и объектами, заинтересованными в событиях ("подписчиками") [BUS96].

Изолируйте и инкапсулируйте блокировки и опросы

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

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

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

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

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

Предпочитайте прерывания опросу

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

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

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

Предпочитайте уведомление о событиях оповещению путем рассылки данных

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

Старайтесь как можно чаще использовать простые механизмы и как можно реже - сложные

Конкретно:

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

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

Активные объекты позволяют уменьшить этот зазор с помощью двух ключевых возможностей:

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

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

Избегайте фанатизма при оптимизации кода

В большинстве систем менее 10% кода используют более 90% времени CPU.

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

Выбор механизмов

Нефункциональные требования и архитектура системы повлияют на выбор механизмов реализации вызовов удаленных процедур.  Ниже приведен обзор возможных компромиссов между имеющимися альтернативами.  

Механизм Применение Комментарии
Сообщения Асинхронный доступ к серверам предприятия Промежуточное программное обеспечение службы сообщений может упростить выполнение задачи программирования приложения за счет поддержки очередей, тайм-аутов и условий восстановления/перезапуска. Промежуточное программное обеспечение службы сообщений можно использовать и в псевдосинхронном режиме. Обычно технология службы сообщений позволяет поддерживать сообщения большого размера. Некоторые подходы RPC могут ограничивать размеры сообщений - в таком случае потребуется дополнительно запрограммировать поддержку больших сообщений.
JDBC/ODBC Вызовы баз данных Это не зависящие от баз данных интерфейсы сервлетов Java или прикладных программ, предназначенные для вызова баз данных, которые могут находиться на том же или другом компьютере.
Стандартные интерфейсы Вызовы баз данных Многие вендоры баз данных реализовали стандартные интерфейсы прикладных программ для своих баз данных. Эти интерфейсы дают выигрыш в производительности по сравнению с ODBC за счет отказа от переносимости приложений.
Вызов удаленных процедур Для вызова программ на удаленных серверах Программировать на уровне RPC не нужно, если у вас есть компоновщик приложений, который сделает это за вас.
Диалоговый Редко применяется в приложениях электронного бизнеса Обычно низкоуровневая связь между программами по протоколам типа APPC или Sockets.

Итоги

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