четверг, июля 26, 2007

Программное обеспечение и параллельная революция (Часть 4)

Херб Саттер (Herb Sutter) и Джэймс Лэрус (James Larus), Microsoft.

Часть 4. Потребности в языках

Что нам надо от программных языков

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

Явная, неявная и автоматическая параллелизация

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

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

Другой широко обсуждаемый подход, это автоматическое распараллеливание, когда компилятор пытается обнаружить параллелизм в программах, написанных на обычных языках, таких как ФОРТРАН. Несмотря на то, что данный подход выглядит очень привлекательно, на практике он работает не так хорошо, как ожидалось. Для понимания возможного поведения программы необходим точный анализ. Этот анализ достаточно сложен для таких простых языков как ФОРТРАН, и на порядки сложнее для таких языков как С, манипулирующих структурами данных на указателях. Кроме того, последовательные программы обычно используют последовательные алгоритмы и содержат мало параллелизма.

Императивные и функциональные языки

Популярные коммерческие языки программирования (такие как, Pascal, C, C++, Java, C#) являются императивными языками, в которых программист шаг за шагом описывает изменения, производимые над переменными и структурами данных. Мелкозернистые управляющие конструкции (такие как циклы), низкоуровневое манипулирование данными, разделяемые, изменяемые (shared mutable) экземпляры объектов делают программы на таких языках сложными для автоматического распараллеливания.

Существует популярная точка зрения, что функциональные языки, такие как Scheme, ML или Haskell могли бы устранить эти препятствия, потому что они естественным образом подходят для параллельного исполнения. Программы, написанные на этих языках, манипулируют неизменяемыми (immutable) экземплярами объектов, которые не подвержены угрозам конкурентного доступа. Кроме того, в отсутствии побочных эффектов (Примечание переводчика, side effects – эффекты изменения глобальных переменных после выполнения подпрограммы.) кажется, что программа имеет меньше ограничений на порядок исполнения.

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

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

Реальный вклад функциональных языков в параллелизм состоит в повсеместном использовании на высоком уровне особого стиля программирования, при котором операции, такие как map или map-reduce, применяются одновременно ко всем элементам составных структур данных. Такие высокоуровневые операции – хороший источник параллелизма. Этот стиль программирования, к счастью, присущ не только чистым функциональным языкам, но также может быть использован в императивных программах.
К примеру, Google Fellows Jeffrey Dean и Sanjay Ghemawat рассказывают как Google использует Map-Reduce для выполнения масштабируемых, высокопроизводительных вычислений (3). Императивные языки могут благоразумно добавлять функциональные расширения и получать от этого существенные выгоды. Это важно, потому что индустрия не может просто начать все с начала. Постепенное добавление поддержки параллельных возможностей очень важно для сохранения огромных инвестиций в существующие приложения, пока существует задел опыта и навыков разработчиков в императивных языках.

Лучшие абстракции

Большинство сегодняшних языков предлагают явную поддержку программирования на уровне потоков и блокировок (threads and locks). Это низкоуровневые абстракции. Поскольку эти конструкции представляют скудный базис для построения абстракций более высокого уровня, это постоянно поощряет разработчиков на использование многопоточного программирования с присущими ему проблемами произвольных блокировок и реентерабельности.

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

Два базовых примера таких высокоуровневых абстракций это асинхронные вызовы (asynchronous calls) и фьючерсы (futures). Асинхронный вызов, это функция или метод, вызов которой происходит в неблокирующем режиме. Вызывающий код продолжает исполняться, а соответствующее сообщение посылается задаче или вилке (task or fork) на выполнение операции независимо. Фьючерс, это механизм для получения результата от асинхронного вызова, это заместитель (placeholder) для значения, которое еще не материализовалось.

Другой пример абстракции высокого уровня, это активный объект, который концептуально исполняется в своем собственном потоке, таким образом, что создав 1000 таких объектов мы создаем 1000 потенциальных потоков исполнения. Активный объект ведет себя как монитор, в котором только один метод может исполняться в данное время, но он не требует для этого использования традиционных блокировок. Вместо этого, внешние вызовы методов активного объекта представляют собой асинхронные сообщения, которые маршализуются и ставятся в очередь на обработку объектом. Активные объекты могут иметь самый разнообразный дизайн, от специализированных языковых конструкций до COM объектов в модели single-threaded apartments, вызываемого из традиционного кода на C.

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

Комментариев нет: