Идеологии параллельности в Java, C#, C, C++, Go и Rust

ideologii parallelnosti v java c c c go i rust

Зачем нам нужен Concurrency

Когда-то было старое доброе время, когда тактовая частота удваивалась каждые 18 месяцев. Это явление было названо законом Мура. Если программа программиста была недостаточно быстрой, они могли подождать, и вскоре компьютеры догнали бы.

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

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

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

Классические примитивы параллельности

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

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

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

  1. Блокировка (также называемая Mutex) — гарантирует, что только один поток выполняется в выбранных областях кода
  2. Мониторы — они делают то же самое, но получше замков, поскольку заставляют вас разблокировать
  3. (Расчет) Семафоры – мощные абстракции, которые могут поддерживать широкий спектр сценариев координации.
  4. Wait-and-notify – делает то же, но слабее, чем Semaphore. Программист должен обрабатывать пропущенные триггеры уведомлений перед ожиданием
  5. Условные переменные позволяют потоку спать и просыпаться, когда возникает определенное условие.
  6. Каналы и буферы с условным ожиданием – прослушивайте и собирайте сообщения, если нет потока для получения (с необязательными ограниченными буферами)
  7. Неблокирующие структуры данных (например, неблокирующая очередь, атомные счетчики) – это разумные структуры данных, позволяющие получить доступ из многих потоков без использования блокировок или минимального количества блокировок.

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

Поддержка языка для примитивов

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

Давайте рассмотрим некоторые из этих вариантов.

Java и C#

Java и C# решили не выбирать вообще. Оба поддерживают все примитивы.

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

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

Единственным примитивом, не поддерживаемым в чистом виде, являются каналы и буферы. Однако если вы хотите их, легко имитировать каналы с очередями и буферами. Хотя ваша реализация никогда не отвечала Go или Erlang по производительности.

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

C и C++

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

Поскольку C и C++ являются языками, более близкими к ОС, передовые исследования потоков часто проводятся с двумя языками. Например, быстрый поиск обнаружил 22 библиотеки параллельности C++ и 6 библиотек параллельности C. Сил достаточно.

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

Эрланг

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

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

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

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

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

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

Если вы хотите узнать больше, прочтите эти статьи: Erlang for Concurrent Programming и The Hitchhiker’s Guide to Concurrency.

Иди

Go очень похож на Эрланга. Его основной режим параллельности – через канал и буферы, и он поддерживает условное ожидание. Его основная философия относительно параллельности: не общайтесь посредством совместного использования памяти; вместо этого делиться памятью путём общения.

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

Подытоживая, Go хочет, чтобы мы программировали параллельность, как Erlang. Однако пока Эрланг придерживается этого, Go доверяет вам, что вы делаете правильные вещи. Если Эрланг драконовский, Го – свободное государство.

Ржавчина

Rust также очень похож на Erlang and Go. Он общается с помощью каналов с буферами и условным ожиданием. Так же, как и Go, он ослабляет ограничения Erlang, позволяя использовать общую память, поддерживая атомный подсчет ссылок и блокировки, а также позволяя передавать каналы от потока в поток.

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

Ниже приведена цитата из документов Rust.

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

Если Эрланг – драконовский, а Го – свободное государство, то Rust – государство-няня.

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

Однако, если у вас нет опыта в параллельности и пытаетесь написать параллельную программу Rust, это вас раздражает. Что бы вы ни делали, оно будет жаловаться на параллельность в загадочном языке. Когда ты изменишься, это скажет что-нибудь другое, а потом снова. Пока вы подробно не разберетесь с параллельностью, это будет непросто.

В отличие от этого, Go дает ложную безопасность программисту, который считает, что его задача, часто ошибочно, выполнена. Они могут заплатить за это позже. Однако они платят только в том случае, если код когда-нибудь попадет в производство, если пользователь когда-нибудь столкнется со сценарием и если будет обнаружена эта ошибка. Это многое «если». Хотя это несправедливо, есть возможность, что программисту это сойти с рук. В любом случае люди не столь хорошо обладают отсроченным удовольствием и долгосрочным взглядом. Поэтому программисты часто предпочитают Go над Rust.

Rust пытается помочь, но редко ценится помощь. Никому не нравится состояние няни.

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

Для получения дополнительной информации прочтите, как примитивы параллельности в Rust сравниваются с примитивами в Go?

Вывод

Когда дело доходит до идеологий параллельности, языки программирования дают вам выбор: свободное состояние (Go), драконовское состояние (Erlang) или состояние няни (Rust).

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

Сначала прочтите Маленькую книгу семафоров, которая научит вас всему о блокировках и семафорах.

Во-вторых, если вы хотите понять каналы и Эрланга, просмотрите MPI. Вы можете подумать, что MPI – мертвый язык. Это не. Большинство научных симуляций по сей день выполняется с помощью MPI. Ею предполагают погоду, с ней конструируют транспортные средства, с ней оказываются наркотики. Наука практически прогрессирует при помощи MPI. MPI использует параллельность так, как мы никогда не могли себе вообразить. Для просмотра просмотрите MPI Communication Primitives.

Если вы придерживаетесь двух вышеуказанных предложений, вы пойдете с оценкой сложности и возможностей параллельности. Это тема, для освоения которой нужна вся жизнь.

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

Добавить комментарий

Ваш адрес email не будет опубликован.