[Перевод] Разбираемся с параллельными и конкурентными вычислениями в Python

image-loader.svg

Я собираюсь рассказать историю о еде, раскрывающую различные возможности конкурентного и параллельного выполнения кода в 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), так как каждая задача должна взаимодействовать с другими, передавая им управление в момент, когда она уже не может сделать ничего полезного.

Реализация «Конкурентных бургеров» с использованием многопоточности

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

Взглянем на «Конкурентные бургеры», реализованные с использованием механизмов многопоточности:

from concurrent.futures import ThreadPoolExecutor
import queues
# Обратите внимание на то, что некоторые методы и переменные тут опущены
# для того чтобы уделить основное внимание вопросам многопоточности.
def run_concurrent_burgers():
    # Создание блокирующих очередей
    customers = queue.Queue()
    orders = queue.Queue(maxsize=5)  # Возможна одновременная обработка до 5 заказов
    cooked_patties = queue.Queue()
    # Гриль совершенно независим от работника,
    # он превращает сырые котлеты в котлеты жареные.
    # Это напоминает чтение данных с диска или выполнение сетевого запроса.
    grill = Grill()
    # Выполнить три задачи, используя пул потоков
    with ThreadPoolExecutor() as executor:
        executor.submit(take_orders, customers, orders)
        executor.submit(cook_patties, grill, cooked_patties)
        executor.submit(make_burgers, orders, cooked_patties)
def take_orders(customers, orders):
    while True:
        customer = customers.get()
        order = take_order(customer)
        orders.put(order)
def cook_patties(grill, cook_patties):
    for position in range(len(grill)):
        grill[position] = raw_patties.pop()
    while True:
        for position, patty in enumerate(grill):
            if patty.cooked:
                cooked_patties.put(patty)
                grill[position] = raw_patties.pop()
        # Не проверять снова в течение минуты
        threading.sleep(60)
def make_burgers(orders, cooked_patties):
    while True:
        patty = cooked_patties.get()
        order = orders.get()
        burger = order.make_burger(patty)
        customer = order.shout_for_customer()
        customer.serve(burger)

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

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

При использовании нескольких потоков мы должны обеспечить такую схему работы с состоянием программы, когда в каждый конкретный момент времени лишь один поток осуществляет чтение или запись данных, к которым могут иметь доступ несколько потоков. Иначе всё может закончиться ситуацией, когда два потока «схватят» одну и ту же котлету, что может очень раздосадовать клиента. Существует такое понятие, как «потокобезопасность» (thread safety), которое имеет отношение к этому вопросу.

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

Сильные стороны многопоточности

  • Операции ввода/вывода не останавливают выполнение других задач.

  • Отличная поддержка различными версиями Python и библиотеками — если нечто может быть запущено в однопоточном режиме — весьма вероятно то, что это заработает и в многопоточном режиме.

Слабые стороны многопоточности

  • Система работает медленнее, чем при применении модуля asyncio из-за того, что на неё ложится дополнительная нагрузка по переключению между системными потоками.

  • Не обеспечивается потокобезопасность.

  • Не ускоряется выполнение задач, зависящих от скорости центрального процессора, наподобие задачи по изготовлению салатов (это — из-за того, что Python поддерживает выполнение в каждый конкретный момент времени лишь одного потока). В результате один работник, одновременно готовящий несколько салатов, не сделает их быстрее, чем если бы делал их один за другим. Дело в том, что в итоге на приготовление одного салата при одновременном приготовлении нескольких салатов уйдёт столько же времени, сколько ушло бы, если салаты готовились бы по одному.

Реализация «Конкурентных бургеров» с использованием модуля asyncio

При использовании модуля asyncio имеется единственный цикл событий, который занимается управлением всеми задачами. Задачи могут пребывать в некотором количестве различных состояний, самыми важными из которых можно назвать состояние готовности (ready) и состояние ожидания (waiting). Цикл событий на каждой итерации проверяет, имеются ли задачи, пребывающие в состоянии ожидания, которые завершены и оказались в состоянии готовности. Затем цикл берёт задачу, находящуюся в состоянии готовности, и выполняет её до тех пор, пока она не завершится, либо — до тех пор, пока не окажется, что ей нужно дождаться завершения другой задачи. Часто подобные задачи представлены операциями ввода/вывода наподобие чтения данных с диска или выполнения HTTP-запроса.

Есть пара ключевых слов, которые используются в большинстве вариантов применения asyncio. Это — async и await.

  • Ключевое слово async используется при объявлении функций для указания на то, что эти функции нужно запускать в виде отдельных задач.

  • Ключевое слово await позволяет создавать новые задачи и передавать управление циклу событий. Это ключевое слово позволяет перевести задачу в состояние ожидания. После завершения новой задачи она оказывается в состоянии готовности.

Посмотрим на реализацию «Конкурентных бургеров» с использованием модуля asyncio:

import asyncio
# Обратите внимание на то, что некоторые методы и переменные тут опущены
# для того чтобы уделить основное внимание вопросам использования asyncio.
def run_concurrent_burgers():
    # Осуществляется управление этими очередями
    customers = asyncio.Queue()
    orders = asyncio.Queue(maxsize=5)  # Возможна одновременная обработка до 5 заказов
    cooked_patties = asyncio.Queue()
    # Гриль совершенно независим от работника,
    # он превращает сырые котлеты в котлеты жареные.
    grill = Grill()
    # Выполняем все задачи с использованием стандартного цикла событий asyncio
    asyncio.gather(
        take_orders(customers, orders),
        cook_patties(grill, cooked_patties),
        make_burgers(orders, cooked_patties),
    )
# Объявляем задачи asyncio с использованием конструкции async def
async def take_orders(customers, orders):
    while True:
        # Разрешаем переключение на новые задачи и здесь, и
        # во всех других await
        customer = await customers.get()
        order = take_order(customer)
        await orders.put(order)
async def cook_patties(grill, cooked_patties):
    for position in range(len(grill)):
        grill[position] = raw_patties.pop()
    while True:
        for position, patty in enumerate(grill):
            if patty.cooked:
                # put_noawait позволяет нам пополнять очередь,
                # не создавая новых задач и не передавая управление
                cooked_patties.put_noawait(patty)
                grill[position] = raw_patties.pop()
        # Ждём 30 секунд перед повторной проверкой
        await asyncio.sleep(30)
async def make_burgers(orders, cooked_patties):
    while True:
        patty = await cooked_patties.get()
        order = await orders.get()
        burger = order.make_burger(patty)
        customer = await order.shout_for_customer()
        customer.serve(burger)

Каждая из функций, олицетворяющих задачи приёма заказа, поджаривания котлеты и приготовления бургера, объявлена с использованием конструкции async def.

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

  • В задаче приёма заказа:

  • В задаче поджаривания котлет:

  • В задаче изготовления бургеров:

Последний кусок этой головоломки заключается в функции run_concurrent_burger, в которой вызывается asyncio.gather для планирования задач, которые должны быть запущены циклом событий, представленным работником «Конкурентных бургеров».

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

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

Сильные стороны применения модуля asyncio

  • Обеспечение чрезвычайно высокой скорости работы при решении задач, зависящих от подсистемы ввода/вывода.

  • Потокобезопасность.

Слабые стороны применения модуля asyncio

  • Задачи, производительность которых зависит от процессора, не ускоряются.

  • Этот модуль появился в Python сравнительно недавно.

Параллельное выполнение кода на практике

В ресторанчике «Параллельные салаты» есть несколько работников, которые делают салаты (все разом). Мы собираемся создать реализацию этого заведения с использованием модуля multiprocessing.

А потом мы ещё заглянем в кофейню, называемую «Облачный кофе» и взглянем на то, как для параллельного выполнения задач можно использовать облачные функции.

Реализация «Параллельных салатов» с использованием модуля multiprocessing

Механизм работы «Параллельных салатов» отлично демонстрирует возможности модуля multiprocessing.

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

import multiprocessing as mp
from concurrent.futures import ProcessPoolExecutor
# Обратите внимание на то, что некоторые методы и переменные тут опущены
# для того чтобы уделить основное внимание вопросам использования multiprocessing.
def run_parallel_salads():
    # Создаём многопроцессные очереди, которые
    # могут обмениваться данными через границы процессов.
    customers = mp.Queue()
    bowls = mp.Queue()
    dirty_bowls = mp.Queue()
    # Запускаем параллельное выполнение задач с использованием ProcessPoolExecutor
    with ProcessPoolExecutor(max_workers=NUM_STAFF) as executor:
        # Set all but one worker making salads
        for _ in range(NUM_STAFF - 1):
            executor.submit(make_salad, customers, bowls, dirty_bowls)
        # Поручаем ещё одному работнику мыть посуду
        executor.submit(wash_bowls, dirty_bowls, bowls)
def make_salad(customers, bowls):
    while True:
        customer = customers.get()
        order = take_order(customer)
        bowl = bowls.get()
        bowl.add(ingredients)
        bowl.add(dressing)
        bowl.mix()
        salad = fill_container(bowl)
        customer.serve(salad)
        dirty_bowls.put(bowl)
def wash_bowls(dirty_bowls, bowls):
    while True:
        bowl = dirty_bowls.get()
        wash(bowl)
        bowls.put(bowl)

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

Так как наши задачи выполняются в различных процессах — они не используют какое-либо обычное состояние Python-программы совместно. У каждого из процессов имеется независимая копия всего состояния программы. Для налаживания «общения» процессов необходимо использовать специальные очереди, поддерживаемые модулем multiprocessing.

Попутная заметка: модули asyncio и multiprocessing

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

import asyncio
from concurrent.futures import ProcessPoolExecutor
process_pool = ProcessPoolExecutor()  # Значение, применяемое по умолчанию, соответствует количеству ядер процессора
async def handle_long_request(n):
    event_loop = asyncio.get_running_loop()
    # Функция calculate_n_pi будет запущена в отдельном процессе, что позволит
    # циклу событий asyncio продолжить параллельную обработку других асинхронных задач
    return await event_loop.run_in_executor(process_pool_executor, calculate_n_pi, n)
def calculate_n_pi(n):
    threading.sleep(60)
    return n * 3.14

Сильные стороны применения модуля multiprocessing

  • Ускорение задач, скорость выполнения которых ограничена возможностями процессора.

  • Потокобезопасность.

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

Слабые стороны применения модуля multiprocessing

  • Отсутствует механизм совместного использования ресурсов.

  • Создаётся большая дополнительная нагрузка на систему. Этот модуль не рекомендуется использовать в задачах, скорость выполнения которых привязана к подсистеме ввода/вывода.

Реализация «Облачного кофе» с использованием облачных функций

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

Ваш друг кофе не выносит, но вы, всё же, вместе решили взять по стаканчику — так сказать, забавы ради. Когда вы подошли к кофейне, каждый из вас попал за собственную стойку со своим баристой, которые медленно выплыли из облака. Баристы приняли у вас заказы, сделали кофе и подали его вам и вашему другу.

Вдруг в «Облачный кофе» влетела оживлённая толпа народа. Стоек на всех не хватило, но совсем скоро из облака выплыли новые стойки с баристами, после чего всех, кто хотел выпить кофе, быстро обслужили. Когда народ попил кофе и дополнительные стойки опустели, эти стойки ещё немного постояли (баристы совершенно не обращали внимания на другие стойки) после чего уплыли обратно в облако.

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

Ваш друг решил сделать абсурдно сложный заказ, чтобы попробовать что-то новое, такое, в чём что-то перебьёт вкус обычного кофе, но заказа он так и не дождался. Бариста добавлял в напиток зефир и шоколадные стружки, но вдруг бросил всё в корзину и крикнул: «Тайм-аут».

Вы, оба на грани нервного срыва, пошли прочь из парка.

Облачные функции — это ещё один механизм ускорения работы кода, на который стоит обратить внимание тем, кто занимается разработкой веб-сервисов. Их, пожалуй, писать легче всего, так как каждая из них отвечает лишь за выполнение отдельного «заказа» за один раз. Применяя их, можно совершенно забыть о конкурентности и о прочих подобных вещах.

def cloud_coffees(order):
    ground_coffee = grind_beans()
    coffee = brew_coffee(ground_coffee)
    coffee.add_embellishments(order)
    return coffee

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

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

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

Наиболее известные реализации этого механизма представлены такими платформами, как AWS Lambda, Azure Functions и Google Cloud Functions.

Сильные стороны облачных функций

  • Чрезвычайно простая модель организации вычислений.

  • Их использование может быть дешевле, чем применение постоянно работающего сервера.

  • Весьма лёгкое масштабирование систем, основанных на таких функциях.

Слабые стороны облачных функций

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

  • Время выполнения вычислений, связанных с запросами, ограничено тайм-аутами.

  • У разработчика нет полного контроля над используемой версией Python — можно использовать лишь те версии, которые предоставлены облачным провайдером.

Какой вариант конкурентного выполнения кода выбрать?

Давайте сведём всё, о чём мы говорили, в единую таблицу.

Многопоточность

Модуль asyncio

Модуль multithreading

Облачные функции

Тип конкурентности

Вытесняющая многозадачность

Кооперативная многозадачность

Многопроцессность

Использование нескольких экземпляров приложения

Конкурентное или параллельное выполнение кода

Конкурентное

Конкурентное

Параллельное

Параллельное

Возможность самостоятельного управления конкурентностью

Отсутствует

Присутствует

Отсутствует

Отсутствует

Принятие решения о переключении между задачами

Операционная система сама выбирает момент переключения между задачами

Задача принимает решение о том, когда ей нужно передать управление другой сущности

Процессы выполняются одновременно, на разных ядрах процессора

Запросы выполняются одновременно в различных экземплярах приложения

Максимальное количество параллельно выполняемых процессов

1

1

Соответствует количеству ядер процессора

Без ограничений

Взаимодействие между задачами

Общее состояние

Общее состояние

Многопроцессные очереди и возвращаемые значения

Невозможно

Потокобезопасность

Отсутствует

Присутствует

Присутствует

Присутствует

Задачи, которые решают с помощью данного подхода

Те, производительность которых ограничена подсистемой ввода/вывода

Те, производительность которых ограничена подсистемой ввода/вывода

Те, производительность которых ограничена возможностями процессора

Те, производительность которых ограничена подсистемой ввода/вывода в том случае, если они выполняются быстрее, чем истекает тайм-аут (около 5 минут)

Дополнительная нагрузка на систему

Использование одного системного потока для решения задач означает необходимость в оперативной памяти и увеличивает время переключения между задачами

Максимально низкая, все задачи выполняются в одном процессе и в одном потоке

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

При запуске новых экземпляров приложения наблюдаются задержки

Теперь, когда вы видите общую картину, вы без труда подберёте именно то, что вам нужно.

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

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

image-loader.svg

Итоги

Мы рассмотрели различные варианты организации конкурентного выполнения кода в Python:

  • Многопоточность.

  • Использование модуля asyncio.

  • Использование модуля multiprocessing.

Мы, кроме того, уделили некоторое внимание одной из возможностей развёртывания приложений, использование которой упрощает параллельное выполнение Python-кода:

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

О, а приходите к нам работать?

© Habrahabr.ru