Многопоточность в одну строку
Python имеет ужасную репутацию, когда речь идет о возможности параллельных вычислений. Не обращая внимания на типичные рассуждения о его потоках и GIL (который обычно нормально работает), по-моему реальная проблема многопоточности Python не техническая, а педагогическая. Распространенные руководства о библиотеках threading и multiprocessing в целом неплохие, но тяжеловаты для понимания. Они начинаются с глубоких вещей, и заканчиваются до просто применяемых практик.
Традиционный пример
Беглое ознакомление с первыми результатами поискового запроса на тему “Python threading tutorial” показывает, что почти каждый из них основан на использовании какого-либо вспомогательного класса в связке с модулем Queue.
Типичный пример многопоточности вида поставщик-потребитель:
Мда… Просматриваются Java’вские корни.
Что ж, я не хочу, что бы у вас создалось впечатление, будто схема поставщик-потребитель плоха для многопоточной разработки — это определенно не так. На самом деле такой способ хорошо подходит для решения множества задач. Однако, я думаю, что это не подходит для ежедневного применения.
Проблемы (на мой взгляд)
Во-первых, вам нужен шаблонный класс, который делает то, что нужно. Во-вторых, вам нужно организовать очередь, согласно которой будут обрабатываться объекты; и наконец, вам нужны методы для входа в очередь и выхода из очереди что бы делать реальную работу (скорее всего, с участием другой очереди, если вы хотите получать обратную связь или сохранять результаты работы).
Больше воркеров, больше задач
Следующее, что вы вероятно сделаете, это пулл воркеров, что бы выжать из Python больше производительности. Ниже приводится измененный код примера из превосходного руководства по многопоточности от IBM. Это достаточно распространенный сценарий, когда вы распределяете задачи получения веб-страниц на несколько потоков.
Работает отлично, но посмотрите на весь этот код! Здесь методы инициализации, списки потоков для отслеживания работы, и что хуже всего, если вы склонны к обработке блокировок как и я, куча вызовов метода join. А впоследствии будет еще сложнее!
А что было сделано? Да практически ничего. Вышеприведенный код представляет собой хрупкую конструкцию. Это внимательное следование шаблону, это высокая вероятность ошибок (я даже забыл вызвать метод task_done() в объекте очереди пока писал это), и это писать много кода и получать мало функционала. К счастью, есть гораздо лучший способ.
Знакомьтесь: Map
Map — это класная маленькая функция, а главное, проста для распараллеливания вашего Python кода. Для тех, кто не вкурсе, map заимствована из функциональных языков, вроде Lisp’а. Это функция, которая применяет другую функцию к последовательности, например:
Этот код применяет метод urlopen к каждому элементу переданной последовательности и сохраняет полученные результаты в список. Это более-менее эквивалентно следующему коду:
Функция map управляет итерацией последовательности, применяет нужную функцию, и в конце сохраняет все получившиеся результаты в список.
Почему это имеет значение? Потому, что используя определенные библиотеки, map делает использование многопоточности тривиальным!
Функция map с поддержкой многопоточности присутствует в двух библиотеках: multiprocessing, а так же малоизвестная, но неменее замечательная — multiprocessing.dummy.
Отступление: Что это? Никогда не слышал о многопоточном клоне библиотеки multiprocessing под названием dummy? Я тоже не слышал до недавнего времени. Есть всего одно предложение на странице официальной документации библиотеки multiprocessing. И это предложение сводится к “Ах да, эта вещь существует”. Это печально, скажуя вам!
multiprocessing.dummy представляет собой точный аналог модуля multiprocessing. Разница лишь в том, что multiprocessing работает с процессами, а multiprocessing.dummy использует треды (со всеми присущими им ограничениями). Поэтому, все что относится к одной библиотеке, относится и к другой. Это делает переключение между ними довольно простым.
Приступим
Для доступа к map-параллельной функции, сперва нужно импортировать модули в которых она содержится и создать пулл:
Последнее выражение делает то же, что и семистрочная функция build_worker_pool в приведенном ранее примере. А именно, создает кучу доступных воркеров, поготавливает их к выполнению задач, и сохраняет их в переменной, что бы к ним было легко обратиться.
Объекты из пула принимают несколько параметров, но сейчас упоминания стоит только один: processes. Этот параметр устанавливает количество воркеров в пуле. Если оставить это поле пустым, то по умолчанию оно будет равно количеству ядер в вашем процессоре.
В общем случае, если вы используете многопроцессовый пулл для ядро-раздельных задач, то больше ядер означает большуую скорость (я говорю это с многочисленными оговорками). Однако, когда речь идет о многопоточной обработке и делах связанных с сетью, это не так, и будет хорошей идеей поэксперементировать с размером пула.
Если вы запустите слишком много потоков, вы затратите больше времени на переключения между ними, чем на полезную работу, так что в этом случае неплох поизменять параметры до тех пор, пока не найдет оптимальный вариант для вашей задачи.
Итак, теперь, когда созданы воркеры и простой способ распараллеливания в наших руках, давайте перепишем загрузку веб-страниц из предыдущего примера.
Посмотрите на это! Код который на самом деле работает занимает 4 строки, 3 из которых формальны. Функция map сделала то же, что и предыдущий код в 40 строк с такой легкостью! Для проверки я испробовал оба подхода и попробовал различные размеры пула.
Результаты:
Потрясающе! Это так же показывает, почему полезно поэкспериментировать с размером пула. Любой пулл с более чем 9 воркерами быстро приводит в падению прироста скорости (на этом компе).
Реальный пример №2
Создание миниатюр для тысяч изображений
Теперь давайте сделаем что-нибудь процесорно-раздельное! Довольно распространенная задача у меня на работе — это обработка больших коллекций картинок. Одна из таких задач — создание миниатюр. И это можно распараллелить.
Простая однопроцессная реализация
Пример несколько адаптирован, но по сути происходит следующее: каталог с изображениями передается в программу, потом из каталога выбираются все картинки, и наконец создаются миниатюры и сохраняются в отдельный каталог.
На моем компьютере это выполняется за 27.9 секунд для порядка 6000 изображений.
Если мы заменим цикл for параллельной функцией map:
5.6 секунд!
Это серъезный прирост для изменения всего лишь нескольких строчек кода. Продакшен версия еще быстрее, так как в ней разделены процессорные задачи и задачи ввода-вывода на отдельные процессы и потоки — обычный рецепт для кода с учетом блокировок.
Разбираемся с параллельными и конкурентными вычислениями в Python

Я собираюсь рассказать историю о еде, раскрывающую различные возможности конкурентного и параллельного выполнения кода в Python.
Прим. Wunder Fund: для задач, где не критичны экстремально низкие задержки — при сохранении и обработке биржевых данных, мы используем Питон, и естественно применяем описанные в статье подходы. Статья будет полезна начинающим разработчикам.
Мы увидим, что когда один человек одновременно делает несколько дел — это похоже на конкурентность, а когда несколько человек, работая бок о бок, заняты каждый собственным делом — это напоминает параллелизм. Эти ситуации мы разберём на простом и понятном примере закусочных, в которые люди заходят в обеденный перерыв. Такие заведения стремятся обслуживать клиентов как можно быстрее и эффективнее. Потом я покажу реализацию механизмов этих закусочных на Python, а в итоге мы сравним разные возможности одновременного «приготовления нескольких блюд», которые даёт нам этот язык, и разберёмся с тем, в каких ситуациях их применение наиболее оправдано.
А именно, я раскрою здесь следующие вопросы:
Отличия конкурентности от параллелизма.
Различные варианты организации конкурентного выполнения кода (многопоточность, модуль asyncio , модуль multiprocessing , облачные функции) и их сравнение.
Сильные и слабые стороны каждого подхода к организации конкурентного выполнения кода.
Выбор конкретного варианта организации конкурентного выполнения кода с использованием специальной блок-схемы.
В чём отличие между конкурентным и параллельным выполнением кода?
Начнём с определений:
▪ Систему называют конкурентной, если она может поддерживать наличие двух или большего количества действий, находящихся в процессе выполнения в одно и то же время.
▪ Систему называют параллельной, если она может поддерживать наличие двух или большего количества действий, выполняемых в точности в одно и то же время.
Самое главное в этих определениях, их ключевое отличие друг от друга, заключается в словах «в процессе выполнения».
The Art of Concurrency
А теперь перейдём к нашей истории про еду.
В обеденный перерыв вы оказались на улице, на которую раньше не попадали. Там есть два места, где можно перекусить: палатка с надписью «Конкурентные бургеры» и ресторанчик, который называется «Параллельные салаты».
Изделия обоих заведений выглядят завлекательно, правда, перед ними стоят длинные очереди. Поэтому у вас возникает вопрос о том, в каком из них вас обслужат быстрее.
В «Конкурентных бургерах» работает дама среднего возраста. На её руке — татуировка «Python», она во время работы от души хохочет. Она выполняет следующие действия:
Переворачивает жарящиеся котлеты для бургеров.
Накладывает на булочки овощи и котлеты, поливает всё это соусом, выдаёт готовые заказы.
Дама без остановки переключается между этими задачами. Вот она проверяет котлеты на гриле и убирает те, что уже сжарились, потом — принимает заказ, дальше, если есть готовые котлеты, делает бургер, после чего вручает клиенту аппетитный свёрток.
А «Параллельные салаты» укомплектованы командой одинаковых мужчин. На их лицах — дежурные улыбки, во время работы они вежливо переговариваются. Каждый из них делает салат лишь для одного клиента. А именно, каждый принимает заказ, кладёт ингредиенты в чистую миску, добавляет заправку, энергично всё перемешивает, пересыпает получившуюся у него здоровую еду в контейнер, который отдаёт клиенту, а миску отставляет в сторону. А тем временем ещё один работник, такой же, как и остальные, собирает грязные миски и моет их.
Главные различия этих двух заведений заключаются в количестве работников и в том, как именно в них решаются различные задачи:
В «Конкурентных бургерах» одновременно (но не в точности в одно и то же время) выполняется несколько задач. Там имеется единственный работник, который переключается между задачами.
В «Параллельных салатах» несколько задач решается одновременно, в точности в одно и то же время. Здесь имеется несколько работников, каждый из которых в некий момент времени решает лишь одну какую-то задачу.
Вы замечаете, что и там и там клиентов обслуживают с одинаковой скоростью. Женщина в «Конкурентных бургерах» ограничена скоростью, с которой её небольшой гриль способен жарить котлеты. А в «Параллельных салатах» используется труд нескольких мужчин, каждый из которых занят на одном салате и ограничен временем, необходимым на приготовление салата.
Вам становится понятно, что «Конкурентные бургеры» (Concurrent Burgers) ограничены скоростью подсистемы ввода/вывода (I/O bound), а «Параллельные салаты» (Parallel Salads) ограничены возможностями центрального процессора (CPU bound).
Ограничения, связанные с подсистемой ввода/вывода — это когда скорость программы зависит от того, насколько быстро происходит чтение данных с диска или выполнение сетевых запросов. «Подсистемой ввода/вывода» в «Конкурентных бургерах» является процесс приготовления котлет.
Ограничения, связанные с возможностями центрального процессора, ослабляются при повышении быстродействия процессора. Чем он быстрее — тем быстрее работает программа. В «Параллельных салатах» «скорость процессора» — это скорость, с которой сотрудник способен приготовить салат.
Вы не в состоянии принять решение, пять минут пребываете в глубокой задумчивости, а потом ваш товарищ, который уже кое-что знает о «Бургерах» и «Салатах», выводит вас из оцепенения и приглашает вас присоединиться к нему в одной из очередей.
Обратите внимание на то, что «Параллельные салаты» можно назвать и конкурентным и параллельным рестораном. Дело в том, что тут наблюдается «наличие двух или большего количества действий». Параллельное выполнение кода — это разновидность конкурентного выполнения кода.
Эти два заведения помогают осознать суть различия между конкурентным и параллельным выполнением задач. Сейчас мы поговорим о том, как всё это реализуется в Python.
Варианты организации конкурентных вычислений в Python
В Python имеется два механизма, которыми можно воспользоваться для организации конкурентного выполнения кода:
Тут есть и возможность параллельного выполнения кода:
Есть и ещё один вариант организации параллельного выполнения кода, доступный при запуске Python-программ в облачной среде:
Конкурентное выполнение кода на практике
Рассмотрим два возможных варианта реализации «Конкурентных бургеров» с использованием многопоточности и модуля asyncio . В обоих случаях имеется единственный рабочий процесс, который принимает заказы, жарит котлеты и делает бургеры.
И там и там используется лишь один процессор. Он переключается между различными задачами, которые ему нужно решить. Разница между применением многопоточности и модуля asyncio заключается в том, как принимаются решения о смене задач:
При использовании многопоточности операционная система знает о наличии различных потоков и может в любое время прерывать их работу и переключать на другую задачу. Сама программа это не контролирует. Это — то, что называется «вытесняющей многозадачностью» (preemptive multitasking), так как операционная система может принудить поток выполнить переключение на другую задачу. В большинстве языков программирования потоки выполняются параллельно, но в Python в каждый конкретный момент времени позволено выполняться лишь одному потоку.
При использовании модуля asyncio программа сама принимает решение о том, когда ей нужно переключиться между задачами. Каждая задача взаимодействует с другими задачами, передавая им управление тогда, когда она к этому готова. Поэтому такая схема работы называется «кооперативной многозадачностью» (cooperative multitasking), так как каждая задача должна взаимодействовать с другими, передавая им управление в момент, когда она уже не может сделать ничего полезного.
Реализация «Конкурентных бургеров» с использованием многопоточности
При использовании многопоточности рабочий процесс меняет задачи в любой момент выполнения кода. Сам рабочий процесс (в нашем случае — дама средних лет) может находиться в процессе приёма заказа, когда его внезапно отвлекают и предлагают проверить жарящиеся котлеты или сделать бургер, а после этого его могут «переключить» на выполнение любой другой задачи.
Взглянем на «Конкурентные бургеры», реализованные с использованием механизмов многопоточности:
Каждая из задач (принять заказ, пожарить котлету, сделать бургер) представляет собой бесконечный цикл, в котором выполняются соответствующие действия.
В run_concurrent_burger мы запускаем каждую из этих задач в отдельном потоке. Мы можем создать поток для каждой из задач и вручную, но есть гораздо более приятный способ это сделать, который заключается в использовании интерфейса ThreadPoolExecutor , который создаёт поток для каждой переданной ему задачи.
При использовании нескольких потоков мы должны обеспечить такую схему работы с состоянием программы, когда в каждый конкретный момент времени лишь один поток осуществляет чтение или запись данных, к которым могут иметь доступ несколько потоков. Иначе всё может закончиться ситуацией, когда два потока «схватят» одну и ту же котлету, что может очень раздосадовать клиента. Существует такое понятие, как «потокобезопасность» (thread safety), которое имеет отношение к этому вопросу.
Для того чтобы обеспечить безопасную работу потоков мы используем очереди (Queues), позволяющие передавать управление состоянием программы между потоками. В пределах отдельных задач очередь блокируется при вызове get до тех пор, пока мы не обслужим клиента, не выполним заказ или не сжарим котлету. Операционная система не пытается переключиться на заблокированный поток, что даёт нам безопасный способ передачи состояния между потоками. Поток, помещая состояние в очередь, не использует его, а затем сообщает о том, что, в процессе его использования, не будет его менять.
Сильные стороны многопоточности
Операции ввода/вывода не останавливают выполнение других задач.
Отличная поддержка различными версиями Python и библиотеками — если нечто может быть запущено в однопоточном режиме — весьма вероятно то, что это заработает и в многопоточном режиме.
Слабые стороны многопоточности
Система работает медленнее, чем при применении модуля asyncio из-за того, что на неё ложится дополнительная нагрузка по переключению между системными потоками.
Не обеспечивается потокобезопасность.
Не ускоряется выполнение задач, зависящих от скорости центрального процессора, наподобие задачи по изготовлению салатов (это — из-за того, что Python поддерживает выполнение в каждый конкретный момент времени лишь одного потока). В результате один работник, одновременно готовящий несколько салатов, не сделает их быстрее, чем если бы делал их один за другим. Дело в том, что в итоге на приготовление одного салата при одновременном приготовлении нескольких салатов уйдёт столько же времени, сколько ушло бы, если салаты готовились бы по одному.
Реализация «Конкурентных бургеров» с использованием модуля asyncio
При использовании модуля asyncio имеется единственный цикл событий, который занимается управлением всеми задачами. Задачи могут пребывать в некотором количестве различных состояний, самыми важными из которых можно назвать состояние готовности (ready) и состояние ожидания (waiting). Цикл событий на каждой итерации проверяет, имеются ли задачи, пребывающие в состоянии ожидания, которые завершены и оказались в состоянии готовности. Затем цикл берёт задачу, находящуюся в состоянии готовности, и выполняет её до тех пор, пока она не завершится, либо — до тех пор, пока не окажется, что ей нужно дождаться завершения другой задачи. Часто подобные задачи представлены операциями ввода/вывода наподобие чтения данных с диска или выполнения HTTP-запроса.
Есть пара ключевых слов, которые используются в большинстве вариантов применения asyncio . Это — async и await .
Ключевое слово async используется при объявлении функций для указания на то, что эти функции нужно запускать в виде отдельных задач.
Ключевое слово await позволяет создавать новые задачи и передавать управление циклу событий. Это ключевое слово позволяет перевести задачу в состояние ожидания. После завершения новой задачи она оказывается в состоянии готовности.
Посмотрим на реализацию «Конкурентных бургеров» с использованием модуля asyncio :
Каждая из функций, олицетворяющих задачи приёма заказа, поджаривания котлеты и приготовления бургера, объявлена с использованием конструкции async def .
В пределах этих задач работник переключается на новую задачу каждый раз, когда используется ключевое слово await . А именно, это происходит в следующих ситуациях:
В задаче приёма заказа:
В задаче поджаривания котлет:
В задаче изготовления бургеров:
Последний кусок этой головоломки заключается в функции run_concurrent_burger , в которой вызывается asyncio.gather для планирования задач, которые должны быть запущены циклом событий, представленным работником «Конкурентных бургеров».
Как мы уже знаем, в точности тогда, когда осуществляется переключение задач, нам не нужно заботиться об управлении совместным доступом к состоянию программы. Мы можем это реализовать, всего лишь воспользовавшись списком очередей и зная то, что у нас не возникнет ситуации, когда две задачи случайно «ухватятся» за одну котлету. Но для решения подобных задач настоятельно рекомендуется пользоваться очередями asyncio , так как это позволит нам очень просто наладить взаимодействие между задачами, указывая на особые моменты в процессах выполнения задач, в которые их можно приостанавливать.
Одним из интересных аспектов использования модуля asyncio является то, что ключевое слово async меняет интерфейс функции, что приводит к невозможности вызова таких функций из обычных функций, не являющихся асинхронными. Это можно счесть как неудачным, так и удачным решением. С одной стороны, можно сказать, что это вредит возможностям по композиции функций, так как нельзя смешивать asyncio -функции с функциями обычными. А с другой стороны, если возможности asyncio используются лишь для организации операций ввода/вывода, это заставляет программиста чётко разделять логику ввода/вывода и бизнес-логику приложения, ограничивая применение asyncio теми частями приложения, которые взаимодействуют с внешним миром, делая код понятнее, упрощая его тестирование. Явное выделение операций, отвечающих за ввод/вывод данных — это довольно-таки распространённая практика в типизированных функциональных языках, а в Haskell это обязательно.
Сильные стороны применения модуля asyncio
Обеспечение чрезвычайно высокой скорости работы при решении задач, зависящих от подсистемы ввода/вывода.
Слабые стороны применения модуля asyncio
Задачи, производительность которых зависит от процессора, не ускоряются.
Этот модуль появился в Python сравнительно недавно.
Параллельное выполнение кода на практике
В ресторанчике «Параллельные салаты» есть несколько работников, которые делают салаты (все разом). Мы собираемся создать реализацию этого заведения с использованием модуля multiprocessing .
А потом мы ещё заглянем в кофейню, называемую «Облачный кофе» и взглянем на то, как для параллельного выполнения задач можно использовать облачные функции.
Реализация «Параллельных салатов» с использованием модуля multiprocessing
Механизм работы «Параллельных салатов» отлично демонстрирует возможности модуля multiprocessing .
Каждый работник в этом заведении представлен новым процессом, создаваемым операционной системой. Эти процессы создаются посредством класса ProcessPoolExecutor , который назначает каждому из них задачи.
При использовании модуля multiprocessing каждая задача выполняется в отдельном процессе. Эти процессы параллельно и независимо друг от друга выполняются операционной системой и друг друга не блокируют. Количество процессов, которые реально можно выполнять параллельно, ограничено количеством ядер процессора. Поэтому мы ограничим соответствующим числом количество сотрудников ресторана, которые делают салаты.
Так как наши задачи выполняются в различных процессах — они не используют какое-либо обычное состояние Python-программы совместно. У каждого из процессов имеется независимая копия всего состояния программы. Для налаживания «общения» процессов необходимо использовать специальные очереди, поддерживаемые модулем multiprocessing .
Попутная заметка: модули asyncio и multiprocessing
Один из сценариев использования модуля multiprocessing заключается в том, чтобы снимать нагрузку, связанную с выполнением тяжёлых вычислительных задач, с asyncio -приложений, чтобы такие задачи не блокировали бы другие части приложения. Вот небольшой набросок, иллюстрирующий этот сценарий:
Сильные стороны применения модуля multiprocessing
Ускорение задач, скорость выполнения которых ограничена возможностями процессора.
Этот модуль может быть использован для организации длительных вычислений, выполняемых на веб-серверах в отдельных процессах.
Слабые стороны применения модуля multiprocessing
Отсутствует механизм совместного использования ресурсов.
Создаётся большая дополнительная нагрузка на систему. Этот модуль не рекомендуется использовать в задачах, скорость выполнения которых привязана к подсистеме ввода/вывода.
Реализация «Облачного кофе» с использованием облачных функций
Вы с другом пошли в парк, чтобы съесть то, что удалось раздобыть на обед, и заметили пушистое разноцветное облако, висящее над группой людей. Вы присмотрелись и поняли, что это — вывеска кофейни «Облачный кофе».
Ваш друг кофе не выносит, но вы, всё же, вместе решили взять по стаканчику — так сказать, забавы ради. Когда вы подошли к кофейне, каждый из вас попал за собственную стойку со своим баристой, которые медленно выплыли из облака. Баристы приняли у вас заказы, сделали кофе и подали его вам и вашему другу.
Вдруг в «Облачный кофе» влетела оживлённая толпа народа. Стоек на всех не хватило, но совсем скоро из облака выплыли новые стойки с баристами, после чего всех, кто хотел выпить кофе, быстро обслужили. Когда народ попил кофе и дополнительные стойки опустели, эти стойки ещё немного постояли (баристы совершенно не обращали внимания на другие стойки) после чего уплыли обратно в облако.
Когда вы попили кофе и вышли — вы обратили внимание на то, что в кофейне всегда находится примерно одинаковое количество стоек, так как туда постоянно подходят люди, делающие заказы. Если в кофейне оказывается больше людей, чем обычно, из облака выплывают новые стойки, а когда случается так, что заказ выполнен, клиент ушёл, а стойка оказалась никому не нужной, она ещё немного пустует, а потом возвращается в облако.
Ваш друг решил сделать абсурдно сложный заказ, чтобы попробовать что-то новое, такое, в чём что-то перебьёт вкус обычного кофе, но заказа он так и не дождался. Бариста добавлял в напиток зефир и шоколадные стружки, но вдруг бросил всё в корзину и крикнул: «Тайм-аут».
Вы, оба на грани нервного срыва, пошли прочь из парка.
Облачные функции — это ещё один механизм ускорения работы кода, на который стоит обратить внимание тем, кто занимается разработкой веб-сервисов. Их, пожалуй, писать легче всего, так как каждая из них отвечает лишь за выполнение отдельного «заказа» за один раз. Применяя их, можно совершенно забыть о конкурентности и о прочих подобных вещах.
Каждый запрос обслуживается отдельным экземпляром всего приложения. Когда создаётся новый экземпляр приложения — имеется небольшая задержка, связанная с выделением ресурсов и запуском этого экземпляра приложения. Именно поэтому экземпляры приложения могут какое-то время простаивать, ожидая поступления новых запросов. Запросы, если они поступают уже запущенному экземпляру приложения, могут быть выполнены практически мгновенно, без задержек. А вот после того, как новых запросов некоторое время не поступает — бездействующие экземпляры приложения уничтожаются, ресурсы, занятые ими, возвращаются системе.
Обработка каждого запроса через некоторое время, зависящее от реализации системы, завершается по тайм-ауту. Нужно обеспечить, чтобы задачи завершались бы до наступления этого тайм-аута, или они будут попросту исчезать, так и не выполнившись.
Экземпляры приложения не могут взаимодействовать с другими экземплярами приложения. Они ни при каких условиях не должны хранить состояние приложения между вызовами, так как они могут быть когда угодно уничтожены.
Наиболее известные реализации этого механизма представлены такими платформами, как AWS Lambda, Azure Functions и Google Cloud Functions.
Сильные стороны облачных функций
Чрезвычайно простая модель организации вычислений.
Их использование может быть дешевле, чем применение постоянно работающего сервера.
Весьма лёгкое масштабирование систем, основанных на таких функциях.
Слабые стороны облачных функций
При запуске новых экземпляров приложений могут наблюдаться задержки.
Время выполнения вычислений, связанных с запросами, ограничено тайм-аутами.
У разработчика нет полного контроля над используемой версией Python — можно использовать лишь те версии, которые предоставлены облачным провайдером.
Какой вариант конкурентного выполнения кода выбрать?
Давайте сведём всё, о чём мы говорили, в единую таблицу.
Многопоточность
Модуль asyncio
Модуль multithreading
Облачные функции
Использование нескольких экземпляров приложения
Конкурентное или параллельное выполнение кода
Возможность самостоятельного управления конкурентностью
Принятие решения о переключении между задачами
Операционная система сама выбирает момент переключения между задачами
Задача принимает решение о том, когда ей нужно передать управление другой сущности
Процессы выполняются одновременно, на разных ядрах процессора
Запросы выполняются одновременно в различных экземплярах приложения
Максимальное количество параллельно выполняемых процессов
Соответствует количеству ядер процессора
Взаимодействие между задачами
Многопроцессные очереди и возвращаемые значения
Задачи, которые решают с помощью данного подхода
Те, производительность которых ограничена подсистемой ввода/вывода
Те, производительность которых ограничена подсистемой ввода/вывода
Те, производительность которых ограничена возможностями процессора
Те, производительность которых ограничена подсистемой ввода/вывода в том случае, если они выполняются быстрее, чем истекает тайм-аут (около 5 минут)
Дополнительная нагрузка на систему
Использование одного системного потока для решения задач означает необходимость в оперативной памяти и увеличивает время переключения между задачами
Максимально низкая, все задачи выполняются в одном процессе и в одном потоке
Использование отдельного системного процесса на каждую задачу приводит к потреблению памяти, превышающему то, что характерно для многопоточных систем
При запуске новых экземпляров приложения наблюдаются задержки
Теперь, когда вы видите общую картину, вы без труда подберёте именно то, что вам нужно.
Но, правда, прежде чем делать окончательный выбор, нужно как следует подумать о самой необходимости ускорения задач. Если некая задача запускается раз в неделю, а на её выполнение нужно 10 минут — есть ли смысл в её ускорении?
Если же вы выяснили, что смысл в ускорении какой-то задачи определённо есть, можете воспользоваться следующей блок-схемой для того, чтобы точно определиться с выбором подхода к её выполнению, который подойдёт именно вам.

Итоги
Мы рассмотрели различные варианты организации конкурентного выполнения кода в Python:
Использование модуля asyncio .
Использование модуля multiprocessing .
Мы, кроме того, уделили некоторое внимание одной из возможностей развёртывания приложений, использование которой упрощает параллельное выполнение Python-кода:
Теперь вы знакомы с особенностями этих подходов к написанию программ и с их сильными и слабыми сторонами, знаете о том, для решения каких задач они подходят лучше всего.
О, а приходите к нам работать?
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
Присоединяйтесь к нашей команде.
Параллельный цикл for в Python

Распараллеливание цикла означает параллельное распределение всех процессов с использованием нескольких ядер. Когда у нас много заданий, каждое вычисление не дожидается завершения предыдущего в параллельной обработке. Вместо этого для завершения используется другой процессор.
В этой статье мы распараллелим цикл for в Python.
Используйте модуль multiprocessing для распараллеливания цикла for в Python
Чтобы распараллелить цикл, мы можем использовать пакет multiprocessing в Python, поскольку он поддерживает создание дочернего процесса по запросу другого текущего процесса.
Модуль multiprocessing можно использовать вместо цикла for для выполнения операций над каждым элементом итерации. Можно использовать объект multiprocessing.pool() , поскольку использование нескольких потоков в Python не даст лучших результатов из-за глобальной блокировки интерпретатора.
Используйте модуль joblib для распараллеливания цикла for в Python
Модуль joblib использует многопроцессорность для запуска нескольких ядер ЦП для выполнения распараллеливания цикла for . Он предоставляет легкий конвейер, который запоминает шаблон для простых и понятных параллельных вычислений.
Чтобы выполнить параллельную обработку, мы должны установить количество заданий, а количество заданий ограничено количеством ядер в ЦП или количеством доступных или простаивающих в данный момент.
Функция delayed() позволяет нам указать Python, чтобы через некоторое время был вызван конкретный упомянутый метод.
Функция Parallel() создает параллельный экземпляр с указанными ядрами (в данном случае 2).
Нам нужно создать список для выполнения кода. Затем список передается в параллельную систему, которая формирует два потока и раздает им список задач.
Используйте модуль asyncio для распараллеливания цикла for в Python
Модуль asyncio является однопоточным и запускает цикл обработки событий, временно приостанавливая сопрограмму с помощью методов yield from или await .
Приведенный ниже код будет выполняться параллельно при его вызове, не влияя на ожидание основной функции. Цикл также выполняется параллельно с основной функцией.
How do I parallelize a simple Python loop?
This is probably a trivial question, but how do I parallelize the following loop in python?
I know how to start single threads in Python but I don’t know how to «collect» the results.
Multiple processes would be fine too — whatever is easiest for this case. I’m using currently Linux but the code should run on Windows and Mac as-well.
What’s the easiest way to parallelize this code?
![]()
![]()
15 Answers 15
Using multiple threads on CPython won’t give you better performance for pure-Python code due to the global interpreter lock (GIL). I suggest using the multiprocessing module instead:
Note that this won’t work in the interactive interpreter.
To avoid the usual FUD around the GIL: There wouldn’t be any advantage to using threads for this example anyway. You want to use processes here, not threads, because they avoid a whole bunch of problems.
The above works beautifully on my machine (Ubuntu, package joblib was pre-installed, but can be installed via pip install joblib ).
Edit on Mar 31, 2021: On joblib , multiprocessing , threading and asyncio
- joblib in the above code uses import multiprocessing under the hood (and thus multiple processes, which is typically the best way to run CPU work across cores — because of the GIL)
- You can let joblib use multiple threads instead of multiple processes, but this (or using import threading directly) is only beneficial if the threads spend considerable time on I/O (e.g. read/write to disk, send an HTTP request). For I/O work, the GIL does not block the execution of another thread
- Since Python 3.7, as an alternative to threading , you can parallelise work with asyncio, but the same advice applies like for import threading (though in contrast to latter, only 1 thread will be used; on the plus side, asyncio has a lot of nice features which are helpful for async programming)
- Using multiple processes incurs overhead. Think about it: Typically, each process needs to initialise/load everything you need to run your calculation. You need to check yourself if the above code snippet improves your wall time. Here is another one, for which I confirmed that joblib produces better results:
This IS the easiest way to do it!
You can use asyncio. (Documentation can be found here). It is used as a foundation for multiple Python asynchronous frameworks that provide high-performance network and web-servers, database connection libraries, distributed task queues, etc. Plus it has both high-level and low-level APIs to accomodate any kind of problem.
Now this function will be run in parallel whenever called without putting main program into wait state. You can use it to parallelize for loop as well. When called for a for loop, though loop is sequential but every iteration runs in parallel to the main program as soon as interpreter gets there.
1. Firing loop in parallel to main thread without any waiting

This produces following output:
Update: May 2022
Although this answers the original question, there are ways where we can wait for loops to finish as requested by upvoted comments. So adding them here as well. Keys to implementations are: asyncio.gather() & run_until_complete() . Consider the following functions:
2. Run in parallel but wait for finish

This produces following output:
3. Run multiple loops in parallel and wait for finish

This produces following output:
4. Loops running sequentially but iterations of each loop running in parallel to one another

This produces following output:
Update: June 2022
This in its current form may not run on some versions of jupyter notebook. Reason being jupyter notebook utilizing event loop. To make it work on such jupyter versions, nest_asyncio (which would nest the event loop as evident from the name) is the way to go. Just import and apply it at the top of the cell as:
And all the functionality discussed above should be accessible in a notebook environment as well.