🐍 Конкурентность и параллелизм в Python: в чем разница?
В статье на простых примерах рассматриваются концепции конкурентности и параллелизма в Python и подходы для работы с этими концепциями: многопоточность, сoroutines, asyncio и многопроцессорность.
При разработке сложных систем с большим количеством задач, синхронное выполнение этих задач в конечном итоге приведет к падению производительности.
Конкурентность и параллелизм – это механизмы реализованные для исправления этой ситуации путем «переплетения» нескольких задач, либо путем параллельного выполнения.
Эти механизмы могут показаться одинаковыми, но у них совершенно разные цели.
Конкурентность
Цель конкурентности – предотвратить взаимоблокировки задач путем переключения между ними, когда одна из задач вынуждена ждать внешнего ресурса. Типичный пример – обработка нескольких сетевых запросов.
Вариант реализации конкурентности – запуск запросов по очереди, пока запросы не обработаются. Здесь, однако, возникает проблема со скоростью и эффективностью.
Решение проблемы – запустить запросы одновременно, а затем переключаться между ними по мере получения ответов.
Параллелизм
Пример: Есть 10 рабочих. Не хотелось бы, чтобы 1 рабочий работал, а остальные 9 сидели и ничего не делали.
Чтобы повысить эффективность, требуется разделить работу между ними, чтобы работа выполнялась быстрее.
Параллелизм использует ту же концепцию, которая применяется к аппаратным ресурсам. Речь идет о максимальном использовании этих ресурсов путем запуска процессов или потоков, использующих все ядра процессора, которыми располагает компьютер.
Обе эти концепции полезны для одновременной обработки нескольких задач, но требуется выбрать правильный метод для конкретной задачи.
Конкурентность подходит для задач, которые сильно зависят от внешних ресурсов, а параллелизм – для задач интенсивно использующих ЦП.
Как работать с этими концепциями в Python?
Python предоставляет механизмы для реализации конкурентности и параллелизма : для конкурентности используется многопоточность и асинхронность, для параллелизма используется многопроцессорность.
Многопоточность
Поток похож на последовательную программу: у него есть начало, процесс работы и окончание работы. В момент выполнения потока существует единственная точка выполнения, однако поток не программа — поток не может выполнятся сам по себе.
Поток выполнения — одна или несколько функций, которые возможно выполнить, независимо от остальной части программы.
Рассмотрим два способа обработки потоков в Python:
С помощью библиотек потоков.
С помощью ThreadPoolExecutor, созданного в качестве менеджера контекста. Это простой способ создавать и уничтожать пул.
В приведенном выше примере создается функция, которая запускается в отдельных потоках. Эта функция запускает поток, а затем засыпает, имитируя некоторое внешнее время ожидания.
В функции main видно, как реализованы оба метода:
первый в строках 9-19.
второй в строках 23-24.
В этом куске кода использована многопоточность для одновременного чтения данных из нескольких URL-адресов, выполнения нескольких экземпляров thread_function() и сохранения результатов в списке.
Как видно из примера, ThreadPoolExecutor упрощает обработку необходимых потоков. Хотя это простой пример, возможно отправить больше URL-адресов, используя ThreadPoolExecutor не дожидаясь каждого ответа.
Из приведенных выше примеров видно, что потоки — это удобный и понятный способ обработки задач, ожидающих других ресурсов. Также видно, что стандартная библиотека Python поставляется с высокоуровневыми реализациями, которые еще больше упрощают эту задачу.
Coroutines (сопрограммы) – альтернативный способ одновременного выполнения функции посредством специальных конструкций, а не системных потоков.
Сопрограмма – общая структура управления, в которой управление потоком совместно передается между двумя подпрограммами без возврата. Получаем однопоточную однопроцессорную конструкцию, использующую кооперативную многозадачность.
В то время, как многопоточность берет и запускает функции в отдельных потоках, asyncio работает в одном потоке и разрешает циклу обработки событий программы взаимодействовать с несколькими задачами, чтобы каждая из них выполнялась по очереди в оптимальное время.
Реализуем это на Python с помощью библиотеки asyncio, предоставляющей основу и API для запуска и управления сопрограммами с ключевыми словами async и await.
Пример:
Пример:
В отличие от работы с потоками, где любая функция выполняется в потоке, в сопрограмме четко указывается в синтаксисе, какие функции выполняются параллельно.
Также сопрограммы не связаны архитектурными ограничениями как потоки и требуют меньше памяти из-за того, что выполняются в одном потоке.
Однако, требуются, чтобы код был написан с использованием собственного синтаксиса, плохо сочетающегося с синхронным кодом.
Многопроцессорность
Многопроцессорность – механизм, разрешающий параллельно выполнять ресурсоемкие задачи, запуская независимые экземпляры интерпретатора Python.
Каждый экземпляр получает код и данные, необходимые для выполнения рассматриваемой задачи, и выполняется независимо в собственном потоке.
Реализуем приведенные выше примеры с использованием многопроцессорной обработки, чтобы увидеть отличия.
Пример выше показывает многопроцессорную обработку, а ниже та же функциональность чтения данных с несколькими URL одновременно.
В фрагменте выше объект Pool() представляет повторно используемую группу процессов, которая сопоставляет итерации для распределения между каждым экземпляром функции.
Преимущество: каждая операция выполняется в отдельной среде выполнения Python и на полном ядре ЦП, что разрешает одновременно запускать процессы интенсивно использующие ЦП.
Недостаток: каждый подпроцесс требует копию данных (с которой подпроцесс работает), отправленных из главного процесса и, как правило, возвращающих данные в главный процесс.