Пример синхронного кода Python быстрее, чем асинхронный - PullRequest
2 голосов
/ 07 мая 2019

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

Асинхронная версия

import asyncio, time

data = {}

async def process_usage(key):
    data[key] = key

async def main():
    await asyncio.gather(*(process_usage(key) for key in range(0,1000000)))

s = time.perf_counter()
results = asyncio.run(main())
elapsed = time.perf_counter() - s
print(f"Took {elapsed:0.2f} seconds.")

Это занимает 19 секунд.Код перебирает ключи 1M и создает словарь, data с тем же ключом и значением.

$ python3.7 async_test.py
Took 19.08 seconds.

Синхронная версия

import time

data = {}

def process_usage(key):
    data[key] = key

def main():
    for key in range(0,1000000):
        process_usage(key)

s = time.perf_counter()
results = main()
elapsed = time.perf_counter() - s
print(f"Took {elapsed:0.2f} seconds.")

Это занимает 0,17секунд!И делает то же самое, что и выше.

$ python3.7 test.py
Took 0.17 seconds.

Асинхронная версия с create_task

import asyncio, time

data = {}

async def process_usage(key):
    data[key] = key

async def main():
    for key in range(0,1000000):
        asyncio.create_task(process_usage(key))

s = time.perf_counter()
results = asyncio.run(main())
elapsed = time.perf_counter() - s
print(f"Took {elapsed:0.2f} seconds.")

Эта версия снижает скорость до 11 секунд.

$ python3.7 async_test2.py
Took 11.91 seconds.

Почему это происходит?

В моем производственном коде у меня будет блокирующий вызов в process_usage, где я сохраню значение ключа в базе данных redis.

Ответы [ 2 ]

6 голосов
/ 07 мая 2019

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

Более разумное сравнение было бы для синхронной версии, чтобы попытаться распараллелить вещи способом, идиоматическим для синхронного кода: используя потоки. Конечно, вы не сможете создать отдельный поток для каждого process_usage, потому что, в отличие от asyncio со своими задачами, ОС не позволит вам создать миллион потоков. Но вы можете создать пул потоков и кормить его задачами:

def main():
    with concurrent.futures.ThreadPoolExecutor() as executor:
        for key in range(0,1000000):
            executor.submit(process_usage, key)
        # at the end of "with" the executor automatically
        # waits for all futures to finish

В моей системе это занимает ~ 17 с, в то время как версия asyncio - ~ 18 с. (Более быстрая версия asyncio занимает ~ 13 с.)

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

Даже примеры игрушек имеют значение для обучения, если ничего больше. Если вы используете такие микробенчмарки для принятия решений, я советую приложить еще больше усилий, чтобы примеры были более реалистичными. Сопрограмма в примере asyncio должна содержать хотя бы один await, а в примере синхронизации должны использоваться потоки для эмуляции того же уровня параллелизма, который вы получаете с помощью async. Если вы настроите оба варианта так, чтобы они соответствовали вашему фактическому варианту использования, тогда эталонный тест фактически даст вам возможность принять (более) обоснованное решение.

1 голос
/ 07 мая 2019

Почему это происходит?

TL; DR

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

Подробно

asyncio - это не волшебство, позволяющее ускорить произвольное ускорениекод.С или без asyncio ваш код все еще выполняется ЦП с предельной производительностью.

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

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

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

...