Асинхронное программирование на Python с помощью asyncio

Статья посвящена отважным программистам, которые желают изучать асинхронное программирование на Python с использованием библиотеки asyncio.

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

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

Допустим, у вас есть скрипт, который запрашивает данные от трех разных серверов. Иногда выполнение запроса к одному из этих серверов может неожиданно занять слишком много времени. Представьте, что получение данных со второго сервера занимает 10 секунд. Пока вы ждете, весь сценарий фактически ничего не делает.

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

Тем не менее, асинхронный код можно не использовать, если вам нужен простой скрипт, практически без ввода/вывода (I/O).

Начнем

программирование на Python

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

  • Coroutine (сопрограмма) – генератор, который получает данные, но не генерирует их. В Python 2.5 был введен новый синтаксис, позволяющий отправлять значения генератору. Мы рекомендуем опробовать любопытный курс по сопрограммам для лучшего понимания происходящего.
  • Tasks – планировщики для сопрограмм. Если вы посмотрите на код ниже, то увидите цикл, в котором запускается _step, а он в свою очередь уже вызывает следующий шаг сопрограммы.
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 <имя_сопрограммы>), текущая сопрограмма приостанавливается, и происходит переключение контекста. Контекст текущей сопрограммы (переменные, состояние) сохраняется и загружается контекст вызванной сопрограммы;
  • если сопрограмма встречает блокирующий код (I/O, sleep), текущая сопрограмма приостанавливается, и управление возвращается в event loop;
  • event loop получает следующие задачи из очереди 2, ...n;
  • затем event loop возвращается к задаче 1, с которой он был прерван.
Как можно увидеть из примера, асинхронное программирование на Python не отличается от аналогичного программирования на других языках.

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

async-await

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

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

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

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)), управление передается обратно в event loop, который запускает другую задачу из очереди (задачу A или задачу B).

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

Причины использования асинхронного программирования

Такие компании, как Facebook, постоянно используют асинхронное программирование на Python. Например, их программное обеспечение React Native и RocksDB использует асинхронные операции. Кроме того, как Twitter обрабатывает более пяти миллиардов сеансов в день?

Производите рефакторинг кода – это поможет извлечь выгоду из асинхронного подхода, и, как результат, программное обеспечение будет работать быстрее.

Оригинал

Другие материалы по теме:

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Go-разработчик
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ