Асинхронность в Python с asyncio

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

Скачивайте книги ТОЛЬКО на русском языке у нас в телеграм канале: PythonBooksRU

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

Что такое асинхронность?

Начнем с того, что при классическом – последовательном – программировании все инструкции, которые вы посылаете интерпретатору, будут выполняться одна за другой. Визуализировать и предсказать выход такого кода легко. Но…

Скажем, у вас есть скрипт, который запрашивает данные с 3 разных серверов. Иногда, по независящим от нас причинам, выполнение запроса к одному из этих серверов может занять неожиданно много времени. Например, представьте, что для получения данных со второго сервера требуется 10 секунд. Пока вы ждете, весь сценарий фактически ничего не делает. Но что если бы вы могли написать скрипт, который вместо ожидания ответа от второго запроса мог бы просто пропустить его и начать выполнение третьего запроса, затем вернуться ко второму и продолжить с того места, где остановился? За счет переключения задач время простоя уменьшается.

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

Кроме того, есть еще одна важная вещь, о которой следует упомянуть. Весь код выполняется в одном потоке. Таким образом, если вы ожидаете, что одна часть программы будет выполняться в фоновом режиме, пока другая будет заниматься чем-то другим, этого не произойдет. Из этого следует, что асинхронный Python подходит для решения I/O bound задач, но никак не для CPU bound.

Надеемся что вы и без нас это понимаете, так как все это относится к основам асинхронного Python. Поэтому давайте сразу перейдем к изучению Python AsyncIO.

Начало работы с учебником по AsyncIO

Вот самые основные определения основных понятий asyncio:

  • Coroutine – это особый вид функций, которые могут быть приостановлены и возобновлены в процессе выполнения. Обычные функци выполняются полностью и возвращают результат. А корутины могут приостанавливаться на определенном этапе и передавать управление другим частям кода, а затем возобновлять выполнение с того же места, где они были приостановлены. Более подробное описание coroutine вы найдете в материале David Beazley “A Curious Course on Coroutines and Concurrency”.
  • Tasks – планировщики для корутин. Если вы посмотрите исходный код ниже, то увидите, что он просто говорит event_loop выполнить свой _step как можно скорее, в то время как _step просто вызывает следующий шаг coroutine.
class Task(futures.Future):
    def __init__(self, coro, loop=None):
        super().__init__(loop=loop)
        ...
        self._loop.call_soon(self._step)
    def _step(self):
            ...
        try:
            ...
            result = next(self._coro)
        except StopIteration as exc:
            self.set_result(exc.value)
        except BaseException as exc:
            self.set_exception(exc)
            raise
        else:
            ...
            self._loop.call_soon(self._step)
  • Event Loop (цикл событий) – это основной механизм в библиотеке asyncio, который управляет выполнением корутин и обрабатывает асинхронные события.

Теперь давайте проверим, как все это работает вместе. Как я уже говорил, асинхронный код выполняется в одном потоке:

Схема выполнения асинхронного кода

Как видно из диаграммы:

  • Event loop выполняется в потоке.
  • Он получает задания из очереди.
  • Каждая задача вызывает следующий шаг корутины.
  • Если корутина вызывает другую корутину (await <coroutine_name> ), текущая приостанавливается и происходит переключение контекста. Контекст текущей корутины (переменные, состояние) сохраняется, а контекст вызываемой загружается и используется дальше.
  • Если программа встречает блокирующий код (I/O, sleep), то текущая корутина приостанавливается и управление передается обратно в цикл событий.
  • Цикл событий получает следующие задачи из очереди 2, …n.
  • Затем цикл событий возвращается к задаче 1 с того места, где он остановился.

Асинхронный и синхронный код

Давайте попробуем доказать, что асинхронный подход действительно работает. Я сравню два скрипта, которые практически идентичны, за исключением метода sleep. В первом я буду использовать стандартный time.sleep, а во втором – asyncio.sleep.

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

Здесь мы используем синхронный sleep внутри асинхронного кода:

import asyncio
import time
from datetime import datetime
async def custom_sleep():
    print('SLEEP', datetime.now())
    time.sleep(1)
async def factorial(name, number):
    f = 1
    for i in range(2, number+1):
        print('Task {}: Compute factorial({})'.format(name, i))
        await custom_sleep()
        f *= i
    print('Task {}: factorial({}) is {}n'.format(name, number, f))
start = time.time()
loop = asyncio.get_event_loop()
tasks = [
    asyncio.ensure_future(factorial("A", 3)),
    asyncio.ensure_future(factorial("B", 4)),
]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
end = time.time()
print("Total time: {}".format(end - start))

Вывод:

Task A: Compute factorial(2)
SLEEP 2017-04-06 13:39:56.207479
Task A: Compute factorial(3)
SLEEP 2017-04-06 13:39:57.210128
Task A: factorial(3) is 6
Task B: Compute factorial(2)
SLEEP 2017-04-06 13:39:58.210778
Task B: Compute factorial(3)
SLEEP 2017-04-06 13:39:59.212510
Task B: Compute factorial(4)
SLEEP 2017-04-06 13:40:00.217308
Task B: factorial(4) is 24
Total time: 5.016386032104492

Теперь тот же код, но с асинхронным методом sleep:

import asyncio
import time
from datetime import datetime
async def custom_sleep():
    print('SLEEP {}n'.format(datetime.now()))
    await asyncio.sleep(1)
async def factorial(name, number):
    f = 1
    for i in range(2, number+1):
        print('Task {}: Compute factorial({})'.format(name, i))
        await custom_sleep()
        f *= i
    print('Task {}: factorial({}) is {}n'.format(name, number, f))
start = time.time()
loop = asyncio.get_event_loop()
tasks = [
    asyncio.ensure_future(factorial("A", 3)),
    asyncio.ensure_future(factorial("B", 4)),
]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
end = time.time()
print("Total time: {}".format(end - start))

Вывод:

Task A: Compute factorial(2)
SLEEP 2017-04-06 13:44:40.648665
Task B: Compute factorial(2)
SLEEP 2017-04-06 13:44:40.648859
Task A: Compute factorial(3)
SLEEP 2017-04-06 13:44:41.649564
Task B: Compute factorial(3)
SLEEP 2017-04-06 13:44:41.649943
Task A: factorial(3) is 6
Task B: Compute factorial(4)
SLEEP 2017-04-06 13:44:42.651755
Task B: factorial(4) is 24
Total time: 3.008226156234741

Как видите, асинхронная версия на 2 секунды быстрее. При использовании асинхронного sleep (каждый раз, когда мы вызываем await asyncio.sleep(1)), управление передается обратно в цикл событий, который запускает другую задачу из очереди (задачу A, либо задачу B).

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

Зачем писать асинхронный код?

Прежде всего, такие компании, как Facebook, часто используют асинхронность. React Native и RocksDB от Facebook считаются асинхронными. Twitter имеет возможным обрабатывать более пяти миллиардов сессий в день тоже благодаря асинхронности.

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

Перевод статьи “Python/Django AsyncIO Tutorial with Examples“.

1 комментарий к “Асинхронность в Python с asyncio”

  1. Пингбэк: Сравнение производительности Golang vs. Python

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *