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

Параллельное программирование не эквивалентно параллельному выполнению, несмотря на то, что эти два термина часто используются как синонимы.

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

Параллельность - это свойство, при котором операции фактически выполняются одновременно. Обычно это определяется аппаратными ограничениями.

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

Итак, что у нас есть в Python? Есть ли в Python встроенные модули, которые облегчают нам создание параллельных программ и позволяют им работать параллельно?

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

Ответ - Jein («да» и «нет» по-немецки). Почему да? Python имеет встроенные библиотеки для наиболее распространенных конструкций параллельного программирования - многопроцессорности и многопоточности. Вы можете подумать, раз уж Python поддерживает и то, и другое, почему именно Jein? Причина в том, что многопоточность в Python на самом деле не является многопоточностью из-за GIL в Python.

threading - это пакет, который предоставляет API для создания потоков и управления ими. Потоки в Python всегда недетерминированы, и их планирование выполняется операционной системой. Однако многопоточность может не делать то, что вы ожидаете.

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

Давайте посмотрим на следующий фрагмент:

import threading
def countdown():
 x = 1000000000
 while x > 0:
 x -= 1
# Implementation 1: Multi-threading
def implementation_1():
 thread_1 = threading.Thread(target=countdown)
 thread_2 = threading.Thread(target=countdown)
 thread_1.start()
 thread_2.start()
 thread_1.join()
 thread_2.join()
# Implementation 2: Run in serial
def implementation_2():
 countdown()
 countdown()

Какая реализация будет быстрее? Давайте проведем отсчет времени.

Удивительно, но запуск 2 countdown() последовательно превосходит многопоточность? Как такое могло произойти? Благодаря пресловутой Global Interpreter Lock (GIL).

Зависит от распространения вашего Python, который в большинстве случаев является реализацией CPython. CPython - это оригинальная реализация Python, подробнее о ней вы можете прочитать в этой ветке StackOverflow.

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

Итак, из нашего фрагмента кода выше, implementation_1 создает 2 потока и должен работать параллельно в многопоточной системе. Однако только один поток может удерживать GIL одновременно, один поток должен ждать, пока другой поток освободит GIL, перед запуском. Между тем, планирование и переключение, выполняемые ОС, создают накладные расходы, которые еще больше замедляются.

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

Возможно использование другой реализации Python, такой как Jython, PyPy или IronPython. Я лично не рекомендую использовать другую реализацию Python, поскольку большинство написанных библиотек не тестируются на разных реализациях Python.

Другой потенциальный обходной путь - использовать C-extension, или более известный как Cython. Обратите внимание, что Cython и CPython - это не одно и то же. Подробнее о Cython можно прочитать здесь.

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

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

Давайте реализуем наш предыдущий фрагмент кода, используя многопроцессорность.

import multiprocessing
# countdown() is defined in the previous snippet.
def implementation_3():
 process_1 = multiprocessing.Process(target=countdown)
 process_2 = multiprocessing.Process(target=countdown)
 process_1.start()
 process_2.start()
 process_1.join()
 process_2.join()

Сам результат не требует пояснений.

Ограничение GIL было чем-то, что меня поймало в самом начале времени как разработчика Python. Я не осознавал, что мое решение использовать потоки совершенно бесполезно, пока не определил время. Надеюсь, эта статья поможет.

Нажмите кнопку 👏, если вы сочтете это полезным.

Эта история опубликована в The Startup, крупнейшем предпринимательском издании Medium, за которым следят + 376 225 человек.

Подпишитесь, чтобы получать наши главные новости здесь.