Как когда-то программисты отказались от неструктурированных переходов, нам необходимо отказаться от прямого использования потоков сейчас и в будущем. И так же, как каждый из нас использует структурные блоки вместо Goto, вместо потоков должны использоваться структуры, построенные поверх них. Благо, все инструменты для этого появились во вполне традиционных языках.
Сперва — немного истории и отсылок с уже состоявшимся обсуждениям.
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. Это позволяет сравнить выразительность и эффективность указанных подходов в различных условиях практических задач.
Спасибо за внимание!
ссылка на оригинал статьи http://habrahabr.ru/company/intel/blog/206030/
Добавить комментарий