http://habrahabr.ru/company/intel/blog/206030/
Сразу раскрою мысль, вынесенную в заголовок. Использование потоков (также именуемых нити, треды,
англ. threads) и средств прямой манипуляции ими (создание, уничтожение, синхронизация) для написания параллельных приложений оказывает столь же пагубное влияние на сложность алгоритмов, качество кода и скорость его отладки, какое вносило использование оператора Goto в последовательных программах.
Как когда-то программисты отказались от неструктурированных переходов, нам необходимо отказаться от прямого использования потоков сейчас и в будущем. И так же, как каждый из нас использует структурные блоки вместо Goto, вместо потоков должны использоваться структуры, построенные поверх них. Благо, все инструменты для этого появились во вполне традиционных языках.
Автор фото: Rainer Zenz
Сперва — немного истории и отсылок с уже состоявшимся обсуждениям.
Goto considered harmful
Наверное, самый авторитетный гвоздь в гроб несчастного оператора в своё время вбил Эдсгер Дейкстра в своей пятистраничной статье 1968 года
«A Case against the GO TO Statement», также известной как «Go-to statement considered harmful».
На Хабре тема использования/изгнания Goto из программ на языках высокого уровня поднималась неоднократно:
habrahabr.ru/post/114211/habrahabr.ru/post/114470/habrahabr.ru/post/114326/
Несомненно, существование Goto — источник нескончаемого холивара. Однако современные языки «общего назначения», приблизительно начиная с Java, не включают в свой синтаксис Goto, по крайней мере в его первозданном виде.
Где Goto ещё в ходу
Отмечу одно часто применяемое, но ещё не упомянутое применение операции прыжка по метке, которое лично меня касается достаточно сильно:
языки ассемблера и машинные коды. Практически все архитектуры микропроцессоров имеют инструкции условных и безусловных переходов. Более того, я не припомню ассемблера, в котором аппаратно сделан оператор
for или
while. В результате программисты, работающие на этом уровне абстракции, вынуждены разбираться со всей мешаниной нелокальных переходов. У Дейкстры по этому поводу есть замечание: "...goto должен быть изгнан из всех высокоуровневых языков (т.е. отовсюду,
кроме — может быть — простого машинного кода)" [в оригинале: «everything except —perhaps— plain machine code»].
Опущу описание всех известных аргументов против Goto; желающие могут найти их по ссылкам выше. Напишу сразу вывод, как его понимаю я:
использование Goto значительно понижает «высокоуровневость» кода, пряча алгоритм в деталях последовательной реализации. Перейдём лучше к потокам.
В чём заключается проблема потоков
Для формулировки того, где ожидать проблем от тредов, обратимся к статье Edward A. Lee
«The Problem with Threads». Её автор попытался привести некоторый формализм (по-моему, излишний) для объяснения следующего факта. Прямое использование потоков требует анализа всех возможных чередований базовых операций, составляющих отдельные нити исполнения. Число таких комбинаций растёт лавинообразно при увеличении размера приложения и быстро превосходит возможности человеческого восприятия и инструментов анализа. Т.е. полностью отладить такую параллельную программу становится невозможно, не говоря уж о формальных доказательствах корректности.
Кроме этого важнейшего аспекта, программирование на потоках (например, на Pthreads) неоптимально просто с точки зрения производительности как программиста, так и результирующего приложения.
- Отсутствие свойства композиции. Вызывая из потока некоторую библиотечную функцию, без анализа её кода нельзя сказать, не породит ли она ещё несколько параллельных нитей исполнения и тем самым превысит возможности аппаратуры (т.н. oversubscription).
- Параллелизм на потоках невозможно сделать необязательным. Он всегда присутствует и жёстко зашит в логику программы несмотря на то, что в реальности два связанных процесса не всегда должны работать одновременно; часто решение должно приниматься динамически с учётом текущей обстановки и наличия ресурсов.
- Сложность обеспечения механизмов балансировки. Даже небольшой перекос в скоростях работы разных потоков может существенно ухудшить производительность всего приложения («караван идёт со скоростью самого медленного верблюда»). Все заботы о том, чтобы аппаратура была равномерно нагружена, перекладываются на прикладного программиста, у которого может и не быть достаточно информации об обстановке в системе. Да и не его это дело, в общем-то — он должен решить прикладную задачу.
Вывод почти дословно повторяет тот, что был сделан чуть выше:
использование потоков значительно понижает «высокоуровневость» кода, пряча алгоритм в деталях параллельной реализации. «Ручное управление» потоками в программе, написанной на языке высокого уровня, обнажает многие детали нижележащей аппаратуры, которые при этом видеть не хочется.
Что же, если не потоки?
Как же использовать возможности многоядерной аппаратуры, не прибегая к потокам? Конечно же, есть различные языки программирования, изначально спроектированные с расчётом на эффективное написание параллельных программ. Тут и Erlang, и функциональные языки. Если нужна экстремальная масштабируемость решения, следует искать ответ в них и предлагаемых ими механизмах. Но что делать программистам, использующим более традиционные языки, например, Си++, и/или работающих с уже существующим кодом?
OpenMP — хорошо, да не то
Довольно долго ни в С, ни в C++ (в отличие от, например, более «молодой» Java) наличие параллелизма в программах никак не было отражено, т.е. фактически было отдано на откуп «сторонним» библиотекам вроде Pthread. Довольно давно известен OpenMP, вносящий структурированный fork-join параллелизм в эти языки, а также в Fortran. По моему мнению, этот стандарт не приносит решений, связанных указанными выше проблемами потоков. Т.е. OpenMP — всё ещё слишком низкоуровневый механизм. Последняя ревизия стандарта не предложила повышения уровня абстракции, а добавила возможностей (и сложностей) тем, кто хочет с помощью OpenMP пускать коды на гетерогенных системах (подробнее про версию 4.0 писали на
Хабре).
Расширения и библиотеки
Между новыми языками, изначально пытающимися поддержать параллелизм, и традиционными языками, полностью его игнорирующими, лежат расширения — попытки добавить необходимые абстракции и закрепить их в синтаксисе, — и библиотеки — завёрнутые в уже существующие концепции языка (такие как вызов подпрограмм) решения проблем. Расширения языков теоретически позволяют добиться лучших результатов, чем библиотеки, ведь с их помощью мы вырываемся из ограничений исходного языка, создавая новый. Но очень нечасто такие расширения завоёвывают популярность у широкой аудитории пользователей. Признание зачастую приходит только после стандартизации такого расширения как части языка.
Расширениями языков и библиотеками, в том числе для параллельного программирования, занимаются многие компании, университеты и комбинации оных. У Intel, есть, конечно же, много раз упоминавшиеся на Хабре варианты и первого, и второго: Intel
Cilk Plus, Intel Threading Building Blocks. Выражу своё мнение, что Cilk (Plus) более интересен как средство повышения уровня абстракции параллелизма чем TBB. Радует наличие поддержки его в
GCC.
C++11
В последних стандартах С++ параллельная природа современных вычислений наконец-то получила признание; возможность кода исполняться одновременно с чем-то ещё учитывается при описании многих языковых конструкций и стандартных классов. Причём на выбор программисту даётся широкий диапазон уровней абстракций: от прямого манипулирования потоками через
std::thread
, через асинхронный вызов
std::packaged_task
до асинхронного/ленивого вызова
std::async
. Большая работа по обеспечению исправной работы всей этой машинерии сдвигается со сторонних библиотек на стандартную, поставляемую с компилятором, реализующим возможности нового стандарта. Открытым (по крайней мере для меня) вопросом является следующий: существуют ли уже реализации C++11, обеспечивающие все три свойства высокоуровневого параллелизма: композицию, необязательность и балансировку, и тем самым освобождающие от этих забот прикладного программиста.
Что ещё почитать
Напоследок хочу поделиться одной книгой. Главная её идея для меня заключается в том, что необходимо внесение понимания о существовании структуры у параллельных приложений в процесс их проектирования. Более того, необходимо обучать этому студентов максимально рано, примерно в то же время, когда им объясняют, почему«goto — это плохо».
Michael McCool, Arch Robison, James Reinders. Structured Parallel Programming — 2012 —
parallelbook.com/.
В книге, в частности, показаны решения одних и тех же задач с использованием нескольких библиотек/языков параллельного программирования: Intel Cilk Plus, OpenMP, Intel TBB, OpenCL и Intel ArBB. Это позволяет сравнить выразительность и эффективность указанных подходов в различных условиях практических задач.
Спасибо за внимание!