85,45
рейтинг
5 января в 21:10

Разработка → Parallelism vs Concurrency: правильно подбираем инструменты перевод

Здравствуйте, уважаемые хабровчане! Мы решили возобновить публикации еще до окончания больших праздников, но в сегодняшней статье все-таки раскрыта тема справедливой раздачи подарков. Сама же статья, как понятно из названия, посвящена сравнительному анализу параллелизма и конкурентности.



конкурент (сущ): соперник, противник

Толковый словарь

Параллельными прямыми называются прямые, которые лежат в одной плоскости и не пересекаются

Википедия

В этой статье я поспорю с Джо Армстронгом и Робом Пайком, продемонстрировав для наглядности, чем отличаются торговые автоматы и коробки с подарками. Иллюстрации сделаны мною, аккуратно отрисованы в программе Microsoft Paint.

Как параллелизм, так и конкуренция — очень модные явления нашего времени. Многие языки и инструменты позиционируются как оптимизированные для параллелизма и конкуренции, причем зачастую и для того, и для другого сразу.

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



Инструменты первого и второго вида могут сосуществовать в одном языке или системе. Например, Haskell, очевидно, хорошо оснащен как для конкуренции, так и для параллелизма. Но, тем не менее, речь все равно идет о двух разных наборах инструментов, и Haskell-вики объясняет, что при реализации параллелизма не следует применять инструменты для конкуренции:

Железное правило: если можете — используйте Чистый Параллелизм, в противном случае используйте Конкуренцию.
Специалисты по Haskell осознают, что две эти задачи не решить при помощи одного инструмента. Аналогичная ситуация складывается в новом языке ParaSail – здесь есть инструменты обоих типов, и в документации рекомендуется не применять конкурентные возможности в параллельных, неконкурентных программах.

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

Аналогично, Джо Армстронг заявляет, что Erlang очень удобен для параллелизма, в силу своей конкурентной природы. Армстронг даже утверждает, что попытки запараллелить унаследованный код, написанный на неконкурентном языке – это «решение не той задачи».

Откуда такие разные мнения? Почему сторонники Haskell и ParaSail думают, что для конкуренции и параллелизма, тогда как специалисты по Go и Erlang считают, что конкурентные языки вполне хороши для параллелизма?

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



В принципе, многие аспекты конкуренции проявляются и в одной очереди, но я нарисовал две, как в оригинале у Армстронга. Что же здесь нарисовано?

  • «Параллелизм» означает, что кока-кола подается быстрее
  • «Параллелизм» означает практически только это – в любом случае, перед нами одна и та же конкурентная задача
  • Кока-колу раньше получит тот, кто раньше встал в очередь
  • В определенном смысле не важно, кто получит кока-колу первым – в любом случае, все ее получат.
  • …кроме, пожалуй, нескольких человечков в конце, так как кока-кола может закончиться – но такова жизнь, дружище. Кому-то приходится быть последним.


На самом деле, вот и все, что можно сказать о торговом автомате. Что насчет раздачи подарков ораве детей? Есть ли разница между баночками с колой и подарками?

Да, есть. Торговый автомат – это система обработки событий: человек может подойти к автомату в любой момент, и всякий раз найдет в ней иное количество баночек. Раздача подарков – это вычислительная система: вы знаете детей, знаете, что купили, решаете, какой ребенок получит какой подарок и каким образом.

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



Чем «конкурентный» отличается от «параллельного» в данном примере?

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


Очередь перед кучей подарков является «конкурентной» в самом прямом смысле: вы хотите добраться до кучи первым, чтобы этот конструктор «Лего» достался именно вам.

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

Вычисления vs обработка событий

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

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

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

Детерминизм: желательно vs невозможно

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

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

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

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

Ох.

Признак надежного параллелизма: детерминизм vs корректность

Как узнать, что при различном следовании событий у вас возникают баги?
Если в вычислительной системе вы все время получаете одинаковый результат, то, вероятно, у вас нет багов с параллелизмом, даже если результат некорректный. Так, если система выдает вам вместо картинок с котиками картинки с собачками, то баг имеется, но он не касается параллелизма, если при каждом прогоне это одни и те же собачки.

В системах обработки событий единственный верный знак отсутствия багов в параллелизме — если вы всегда получаете корректный результат.
Когда два пользователя наперегонки пытаются снять деньги со счета, вы не можете ожидать, что всегда будете получать идентичные результаты. Чего же ожидать, если исходить из исправности банковской программы? Многого. У вас (вероятно) никогда не будет отрицательного баланса на счете, вы (наверное), не сможете опустошить банковский счет дважды, фактически создав лишние деньги и т.д.

Если никогда не происходит ни одной из этих ошибок, то вы реализовали правильно функционирующий банк, в противном случае есть баг. В случае с АТС все примерно так же, за тем исключением, что «корректному результату» будет соответствовать иной набор условий.

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

Ох!

Баги с параллелизмом: легко отловить vs невозможно описать

В случае с помеченными подарочными коробками отследить баги с параллелизмом не составляет труда – даже если подписи на китайском, а вы китайского не знаете:



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

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

Такие инструменты не чисто гипотетические, они существуют. Например, такой инструмент есть в Cilk. В Checkedthreads для этого есть инструмент на основе Valgrind.

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

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

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



Когда кто-то подбирается к подаркам вне очереди – это явное нарушение. Баг обнаружен, порядок восстановлен.

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

Вот как эта проблема выглядит в коде. Следующая ошибочная реализация перевода денег была помечена как неправильная автоматическим отладочным инструментом:

src.balance -= amount
dst.balance += amount


Здесь у нас нет никакой синхронизации – значение src.balance может быть изменено двумя процессами, причем одному не обязательно дожидаться, пока другой закончит работу. Итак, вы можете потерять некоторые операции уменьшения на единицу или что-нибудь еще. Детектор гонки данных, например, Helgrind, заметит этот баг, отслеживая обращения к памяти и обнаружив рассинхронизацию — это не сложнее, чем проверка в Cilk или checkedthreads.

Правда, код, приведенный ниже, по-прежнему содержит баги, но детекторы гонки данных на него не среагируют:

atomic { src.balance -= amount }
atomic { dst.balance += amount }


Здесь «atomic» означает, что перед изменением балансов каждому придется подождать в своеобразной очереди — будем считать ее именно очередью, пусть и очень необычной; подробнее об этом ниже. Когда очередь построена правильно, детекторы гонки данных спокойны – но сохраняется ли здесь баг?

Поток может быть приостановлен после уменьшения src.balance на единицу, но еще до увеличения dst.balance на единицу; возникает промежуточное состояние, в котором деньги «временно потеряны». Это проблема? Возможно. Вы разбираетесь в банковском деле? Я — нет, и Helgrind точно не разбирается.
Вот программа с более явной ошибкой:

if src.balance - amount > 0:
  atomic { src.balance -= amount }
  atomic { dst.balance += amount }


Здесь поток может проверить, есть ли на src.balance достаточная сумма денег, чтобы ее можно было снять, после чего отправится перекурить. Тем временем подойдет другой поток, сделает подобную проверку и станет снимать деньги. Первый поток возвращается, встает в очередь, дожидается своего часа и снимает свою сумму — и эта операция уже может не пройти, если первый поток успеет снять достаточно много денег.
Ситуация напоминает такую: пока вас не было, кто-то явился и унес ваш айфон. Это условия гонки, а не гонка данных — то есть, ситуация возникает независимо от того, насколько аккуратно была построена очередь.

Что такое «гонка условий»? Зависит от приложения. Я не сомневаюсь, что последний фрагмент содержит баги, но насчет предпоследнего не столь уверен, все зависит от того, как работает банк. Если мы даже не можем дать дефиницию «гонки условий», не зная, что именно делает программа, невозможно надеяться, что мы сможем ее автоматически отследить.

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

Очереди: деталь реализации или часть интерфейса

Когда подарки подписаны, очередь не нужна. Каждый может подойти и забрать свой.

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

Вот почему я начертил «логически параллельные» линии, связывающие детей с подарками, именно так, что они пересекаются – хотя фактически конфликтов за конкретные подарки не возникает.

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

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



Напротив, в конкурентной системе очередь находится прямо в интерфейсе:

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


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

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

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

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

Важность вытеснения

Итак, предотвращение или обнаружение конфликтов – необходимый аспект вычислительных систем, не обеспечиваемый конкурентными инструментами. Есть ли такие вещи, которые предоставляются конкурентными инструментами, но не нужны в вычислительных системах?

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

Еще одна вещь, которая, в сущности, не нужна в вычислительных системах – это очень малозатратные вытесняемые потоки/процессы.

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

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

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

Различия между инструментами в одного и того же семейства

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

  • Erlang вообще не позволяет процессам разделять память. Таким образом, гонка данных никогда не возникнет, но это не особенно впечатляет, поскольку, как мы убедились, гонку данных легко выявить автоматически – а гонка условий вообще не устраняется запретом на разделение памяти. Правда, в данном случае хорошо, что вы можете без труда масштабироваться на множество компьютеров, а не просто на несколько ядер в одном и том же процессоре.
  • Rust не позволяет разделять память, если только она не защищена от изменений. Простое масштабирование на несколько компьютеров отсутствует, но работа на каждом компьютере идет эффективнее, отпадает необходимость в детекторах гонки данных, которые могут давать ложноотрицательные результаты из-за плохого покрытия тестами. (Оказалось, что все несколько иначе – вот объяснение, где также упомянуто о планах добавить в Rust инструменты для параллелизма наряду с его конкурентными возможностями.)
  • Go позволяет разделять что угодно, обеспечивая максимальную производительность за счет необходимости вполне подъемной (на мой взгляд) дополнительной работы по верификации. На случай гонок данных в Go есть специальный детектор, а гонки условий так или иначе случаются в любых системах.
  • STM Haskell позволяет свободно разделять неизменяемые данные, если вы явно это запрашиваете. Кроме того, он предоставляет интерфейс для транзакционной памяти – думаю, это классная штука, которая порой с большим трудом эмулируется другими методами. В Haskell есть и другие инструменты для конкуренции – например, каналы, а если вам требуется масштабируемость на несколько машин как в Erlang, то, очевидно, эта задача решаема при помощи Cloud Haskell.


Разумеется, многое зависит и от языка, на котором вы программируете — Erlang, Rust, Go и Haskell, соответственно. А теперь о вычислительных системах:

  • Parallel Haskell просто запараллеливает чистый код. Статическая гарантия отсутствия багов с параллелизмом за счет отсутствия побочных эффектов.
  • ParaSail допускает побочные эффекты, но с ним придется отказаться от многих других вещей, например, от указателей. В результате он будет вычислять те или иные вещи параллельно лишь при условии, что они не разделяют изменяемых данных (например, можно параллельно обработать два элемента массива, если компьютер уверен, что они не пересекаются). Подобно Haskell, в ParaSail в некоторой степени поддерживается конкуренция, а именно «конкурентные объекты», которые могут быть разделяемыми и изменяемыми – и в документации подчеркивается польза от неприменения конкурентных инструментов, если все что вам нужно – это параллелизм.
  • Cilk – это вариант C с ключевыми словами для параллельных циклов и вызовов функций. Этот язык допускает разделяемое использование изменяемых данных (то есть, вы можете выстрелить себе в ногу, если хотите), но оснащен инструментами для детерминированного обнаружения таких багов, если они все-таки попадутся в вашем тестовом вводе. Чем действительно полезно неограниченное совместное использование изменяемых данных – так это возможность обойтись без самострела – то есть, если параллельный цикл нормально ведет вычисления с учетом всех оптимизаций, направленных на устранение побочных эффектов любой локальной задачи, а затем заканчивается, то никто уже не может изменить результат этих вычислений. Вновь метафора: дети получают в подарок каждый по коробочке «Лего», потом каждый строит что-нибудь из своего конструктора, а затем все играют с получившимися игрушками вместе.
  • Система checkedthreads во многом похожа на Cilk; она не завязана на языковые расширения, полностью открыта и свободна – не только интерфейс и исполняющая среда, но и инструменты для поиска багов.


Я написал checkedthreads, так что считайте это небольшой рекламой; checkedthreads – это портируемая, свободная, безопасная среда, которая уже доступна в самых мейнстримовых языках C и C++, в отличие от многих систем, рассчитанных на новые языки или языковые расширения.

Что касается Cilk, ведутся работы по его стандартизации в C++, но Cilk требует добавлять новые ключевые слова, а в сообществе C++ этого не любят. Cilk доступен в ветках gcc и LLVM, но работает пока не на всех платформах (он расширяет ABI). Запатентованы некоторые новые возможности Cilk. Не все они есть в свободном доступе, т.д. и т.п.

Однако важное достоинство Cilk заключается в том, что за его поддержку отвечает Intel, а за поддержку checkedthreads – в сущности, вы сами. Если Cilk вам подходит, и вы решили им пользоваться, почитав мои посты о checkedthreads, то мои усилия по рекламе автоматической отладки параллельных программ не прошли даром.

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

Заключение

Мы обсудили отличия между параллельными, вычислительными системами и конкурентными системами, предназначенными для обработки событий. Основные сферы различий таковы:

  • Детерминизм: желателен vs невозможен
  • Признак параллельной безопасности: одинаковые результаты vs корректные результаты
  • Баги с параллелизмом: легко отловить vs невозможно дать дефиницию
  • Очереди: деталь реализации vs часть интерфейса
  • Вытеснение: практически бесполезно vs практически необходимо


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

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

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

Я считаю, что не лишена логики и моя точка зрения – а именно: «конкуренция – это решение неизбежных конфликтов, связанных с хронометражем, параллелизм – это избегание ненужных конфликтов» – «торговые автоматы vs подписанные подарки». Вот как это выглядит – теперь параллельные стрелки не пересекаются, логически все именно так:



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

Обращение с параллелизмом при помощи специально предназначенных для этого инструментов – не новость. Sawzall — специализированный параллельный язык Роба Пайка, где код гарантированно не содержит багов с параллелизмом, был предтечей конкурентного языка Go, разработанного тем же Пайком.

Однако, в настоящий момент инструменты для конкуренции выглядят более «раскрученными», чем инструменты для параллелизма — причем они позволяют работать с параллелизмом, пусть и относительно плохо. Громкое и плохое часто затмевает менее броское, но хорошее. Было бы грустно, если поддержку параллелизма перестанут совершенствовать, сочтя ее побочным эффектом «приоритетной» конкуренции – или эта поддержка заглохнет там, где уже существует сейчас. Я даже перефразирую выражение «попытка запараллелить последовательный код — это решение не той задачи» вот так: «применение чисто конкурентных инструментов в вычислительном коде – это решение не той задачи». Есть простой факт: код на C, запараллеленный при помощи правильных инструментов, получается быстрее и безопаснее, чем на Erlang.

Итак, все дело в том, чтобы «решать задачу при помощи подходящих инструментов» и никому не давать умыкнуть ваш Apple iPhone.
Автор: @ph_piter Yossi Kreinin

Комментарии (5)

  • –1
    Прикол Go в том, что картинка с двумя очередями и одним автоматом легко превращается в картинку с двумя очередями и двумя автоматами. И все сводится к одной системной переменной GOMAXPROCS. Если использовать горутины, то даже при GOMAXPROCS=1 мы получаем конкурентный код. Наш код, по сути, однопоточен и непараллелен, но он конкурентный. Как только GOMAXPROCS становится равно числу ядер и это число больше 1, то наш код становится еще и параллельным. В силу примитивов языка это делается именно так просто, если конечно конкурентный код правильно написан. И кажется именно об этом говорит Пайк. У него на этому тему отдельная лекция есть.
    • +4
      Лежит у тебя в памяти большой блоб данных (автомат), и каждый процесс его меняет ( очередь). И при чем здесь GO мне совсем не понятно. Конкурентно — да, параллельно — нет.
  • +2
    Странно, сравнивает тёплое с мягким и пишет про это много букв…

    Параллельность может сопутствовать конкурентности, конкурентность может сопутствовать параллельности. Но могут и сами по себе быть.

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

    Там где есть средства разруливания конкурентности при параллельности, есть и параллельность. Зачем ещё много букв сравнений?

    Переписать всё это в ключе обзора случаев — была бы хорошая, полезная статья без страстей и странностей.
    • 0
      Ну так в начале, мне кажется, как раз эти случаи разобрали. Типа конкурентные задачи можно решать параллельно и наооборот.
  • –1
    А версия Cilk под Visual Studio есть?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка