Основные принципы программирования: конкурентность

Рассказывает Аарон Краус 


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

Конкурентность в программировании

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

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

Хотя и считается, что конкурентные вычисления включают в себя параллельные, у них есть существенные отличия.

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

Зачем нужно конкурентное программирование?

Главным преимуществом этого подхода является максимально возможное использование ресурсов системы. В начале 2000-ых стало популярным использование многоядерных процессоров, пришедших на смену одноядерным, пускай и с очень мощным на то время ядром. Это в первую очередь позволяет оптимизировать время выполнения программы путём разделения нагрузки на ядра.  Я не буду углубляться в эту тему — принципы синхронизации процессов и потоков различны в разных ОС — но вы можете почитать об этом, например, здесь.

В современных языках программирования принцип конкурентности обычно реализован в виде процесса многопоточности. Многопоточность позволяет программе работать в нескольких потоках, предоставляя преимущества параллелизации (быстрое исполнение, эффективное использование ресурсов и т.д.), но она также сохраняет и её недостатки (о них ниже), поэтому некоторые языки используют механизм, названный Global Interpreter Lock (GIL). Обычно его можно встретить в стандартных реализациях Python и Ruby (CPython и Ruby MRI соответственно), он предотвращает одновременное исполнение более чем одного потока — даже на многоядерных процессорах. Это похоже на недостаток, но GIL нужен для предотвращения потоконебезопасных активностей. Реализации с GIL обычно повышают скорость работы однопоточных программ и облегчают интеграцию с библиотеками C, но за это приходится платить возможностями многопоточности.

Проблемы конкурентного программирования

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

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

Прим. перев. Рекомендуем также почитать наши материалы по теме многопоточного программирования:

Будущее конкурентного программирования

Конкурентное программирование — это очень мощный инструмент, даже с учётом его недостатков. Несколько языков предоставляют феноменальную поддержку инструментов для использования этого принципа: больше всего это проявляется в API языка Go, созданного в Google. Разработчики других языков, например, Python и Ruby, видят отрицательные стороны конкурентности и поэтому используют GIL по умолчанию. Так или иначе, если вы пишете приложение, которое использует принципы конкурентности, обязательно уделите особое внимание планированию, иначе вы рискуете повредить данные.

Перевод статьи «Programming Concepts: Concurrency»