Async в python - Singulared

Async в python

Асинхронный подход — это типичный пример того, про что говорят «Новое — это хорошо забытое старое». Сам по себе подход появился очень давно, когда надо было эмулировать параллельное выполнение задач на одноядерных процессорах и старых архитектурах.

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

Параллельность - выполнение нескольких блоков кода в один момент времени.

Многозадачность (Mutitasking)

Multitasking

Параллельная обработка (Parallel Processing)

Parallel

Выполнение Задачи (Task Execution)

Task Execution

CPU Bound Tasks

CPU Bound

I/O Bound Tasks

I/O Bound

Асинхронность?

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

С синхронным в целом все понятно — пришел клиент, открылся сокет, передали данные, если это все — сокет закрылся. В этом случае пока мы не закончили обмен данными с одним клиентом — не можем начать его с другим. По такому принципу обычно работают простые серверы, которым не надо держать сотни и тысячи клиентов одновременно. В случае если нагрузка возрастает, но не критично — можно создать еще один или несколько потоков (или даже процессов) и обрабатывать подключения еще и в них. Это обкатанный годами, стабильно работающий подход, который, например, использует сервер Apache. Данные от клиентов обрабатываются в порядке строгой очереди, а в случае запуска какого-то CPU Bound кода — например, каких-то вычислений или хитрого запроса в БД — это все никак не влияет на других клиентов.

Однако в таком подходе есть одна ощутимая проблема. Ресурс потоков и процессов исчерпаем. Как следствие сервер не может масштабироваться бесконечно. И ровно этот момент на помощь приходит неблокирующий ввод-вывод (nonblocking I/O), позволяющий не порождать кучу процессов и потоков и слушать данные от множества клиентов на одном сокете.

Собственно методов (системных вызовов) работы с неблокирующими сокетами не так и много. Самый базовый из них это системный вызов select(), который принимает на вход некоторое количество файловых дескрипторов или сокетов и слушает события на них. При готовности сокета к чтению или записи вызывается соответствующая функция - обработчик (callback handler).

Кроме select существует несколько более оптимизированных методов работы с неблокирующими сокетами, такие как poll/epoll для linux или kqueue/kevent для BSD систем.

Asyncio

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

Модуль asyncio был добавлен в основную библиотеку python в версии 3.4.

Сам модуль предоставляет инфраструктуру для написания однопоточного конкурентного кода при помощи сопрограмм (corutines), мультиплексирования ввода/вывода данных через сокеты и другие ресурсы, запуска сетевых клиентов и серверов, и другие подобные примитивы.

Основной функционал модуля asyncio основан на понятии цикл событий (event loop). Главная функция цикла событий - ожидание какого-либо события и определенная реакция на него. Например, цикл ответственен за обработку задач ввода/вывода, а так же системных событий.

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

Рассмотрим в качестве примера простой web сервер. Большую часть времени который находится в ожидании запроса пользователя, т.е. являющийся ярким примером I/O Bound приложения. Как только на сервер приходит запрос пользователя event loop определяет сокет, на котором то событие произошло и вызывает один или несколько обработчиков этого события. По завершению своей работы эти обработчики возвращают выполнение в цикл событий (event loop). В asyncio для этого используются сопрограммы (coroutines).

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

Ещё один важный термин, это future. Future объект представляет собой некоторое хранилище для результата работы функции которая ещё не завершена. Event loop следит за такими объектами и помечает их выполненными как только появляется результат функции.

Определение собственных сопрограмм

Для python == 3.4 существовал один способ создания сопрограмм

import asyncio

@asyncio.coroutine
def coro():
    pass

Для python >= 3.5 появился более "нативный" вариант.

async def coro():
    pass

В данном случае методы async/await можно рассматривать как некий API для построения асинхронных приложений.

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

import asyncio

async def compute(x, y):
    print("Compute %s + %s ..." % (x, y))
    await asyncio.sleep(1.0)
    return x + y

async def print_sum(x, y):
    result = await compute(x, y)
    print("%s + %s = %s" % (x, y, result))

loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1, 2))
loop.close()

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

Вот как это выглядит с точки зрения интерпретатора.

Sequence diagram of the example