Python 3 - Введение в asyncio

Модуль asyncio был добавлен в основную библиотеку python в версии 3.4 в качестве временного пакета. Это означает, что в ходе обновлений модуль может получить изменения, несвместимые с предыдущими версиями, или, возможно, вообще будет удален из базовой библиотеки. Из документации следует, что asyncio "предоставляет инфраструктуру для написания однопоточного конкурентного кода при помощи сопрограмм (corutines), мультиплексирования ввода/вывода данных через сокеты и другие ресурсы, запуска сетевых клиентов и серверов, и другие подобные примитивы". Эта статья не ставит цели описать всё, что можно сделать при помощи asyncio, однако из неё можно понять, как использовать этот модуль и чем он может быть полезен.

Если вам нужен пакет с функционалом asyncio в более старых версиях python, обратите внимание на Twisted или gevent.

Определения

Основной функционал модуля asyncio основан на понятии цикл событий (event loop). Главная функция цикла событий - ожидание какого-либо события и определенная реакция на него. Например, цикл ответственен за обработку задач ввода/вывода, а так же системных событий. Asyncio содержит несколько реализации цикла событий. По-умолчанию выбирается версия, наиболее эффективная исходя из операционной системы, в которой запущена программа. Однако, имеется возможноть выбрать конкретную реализацию по желанию. В самом общем случае цикл событий как-бы говорит: "когда произойдет событие А, в ответ необходимо запустить функцию Б".

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

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

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

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

async и await

Ключевые слова async и await были добавлены в python в версии языка 3.5, чтобы определять родные сопограммы и отличать их от сопрограмм, основанных на генераторах. Если вас интересует более детальное описание async и await, обратите внимание на PEP 492.

В python версии 3.4 сопрограммы определялись следующим образом:

# Пример сопрограммы на Python 3.4
import asyncio

@asyncio.coroutine
def my_coro():
    yield from func()

В версии языка 3.5 декоратор asyncio.coroutine все еще работает, но модуль types получил обновление в виде функции coroutine, которая может определить с каким типом сопрограмм вы взаимодействуете, родным или нет. Начиная с python 3.5 для того, чтобы определить сопрограмму можно использовать async def. И теперь, функция, определенная выше, будет выглядеть так:

import asyncio

async def my_coro():
    await func()

Определяя сопрограмму сопособом, описанным выше, в теле функции нельзя использовать yeld. Вместо этого, для возвращения значений вызвавшей функции, необходимо использовать return или await. Обратите внимание, что ключевое слово await можно использовать только в функциях, определенных через async def.

Ключевые слова async/await можно рассматривать как api к асинхронному программированию. Asyncio - просто модуль, который использует. Помимо него, есть проект curio, который содержит отдельную реализацию цикла событий и так же использует async / await “под капотом”.

Пример сопрограммы

Хотя знание материальной части об устройстве сопрограмм однозначно полезно, для полного понимания необходимо увидеть несколько примеров их реализации - это позволит освоиться с синтаксисом и собрать воедино всю полученную информацию. Ну что же, с такими мыслями приступим к простому примеру!

Достаточно часто вы сталкиваетесь с задачей загрузки файлов, не важно - со внутреннего ресурса или из интернета. И обычно это больше чем один файл. Так что, давайте напишем пару сопрограмм, которые будут выполнять эту задачу для нас:

import asyncio
import os
import urllib.request

async def download_coroutine(url):
    """
    Сопрограмма для загрузки данных по указанному url
    """
    request = urllib.request.urlopen(url)
    filename = os.path.basename(url)

    with open(filename, 'wb') as file_handle:
        while True:
            chunk = request.read(1024)
            if not chunk:
                break
            file_handle.write(chunk)
    msg = 'Завершена загрузка {filename}'.format(filename=filename)
    return msg

async def main(urls):
    """
    Создет группу сопрограмм и ожидает их завершения
    """
    coroutines = [download_coroutine(url) for url in urls]
    completed, pending = await asyncio.wait(coroutines)
    for item in completed:
        print(item.result())


if __name__ == '__main__':
    urls = ["http://www.irs.gov/pub/irs-pdf/f1040.pdf",
            "http://www.irs.gov/pub/irs-pdf/f1040a.pdf",
            "http://www.irs.gov/pub/irs-pdf/f1040ez.pdf",
            "http://www.irs.gov/pub/irs-pdf/f1040es.pdf",
            "http://www.irs.gov/pub/irs-pdf/f1040sb.pdf"]

    event_loop = asyncio.get_event_loop()
    try:
        event_loop.run_until_complete(main(urls))
    finally:
        event_loop.close()

Мы импортируем необходимые модули и затем создаем нашу первую сопрограмму, используя async синтаксис. Сопрограмма называется download_coroutine и использует библиотеку urllib для загрузки объектов по переданным ссылкам. По окончании, сопрограмма возвращает соответствующее сообщение.

Другая сопрограмма - функция main. Она принимает список ссылок в качестве параметра и занимается постановкой их в очередь. Для того, чтобы отследить момент, когда все сопрограммы завешат свою работу, мы используем функцию wait из asyncio. Чтобы запустить выполнени сопрограммы, её необходимо добавить в цикл событий. Это происходит в самом конце, после определения самого цикла и вызова его метода run_until_complete: именно там мы передаем сопрограмму main в цикл событий. Это запускает нашу основную сопрограмму, которая ставит в очередь вторую сопрограмму и следит за её выполнением. Такой метод известен как связывание сопрограмм.

Запуск по расписанию

Используя asyncio и цикл событий можно так же вызывать обычные функции по расписанию. Первый метод, на который мы обратим внимание, называется call_soon. Он просто вызывает переданную функцию или обработчик события как только появляется возможность. Этот метод работает как FIFO очередь, так что если некоторые функции выполняются достаточно долго, выполнение последующих функций будет отложено до завершения предыдущих. Давайте рассмотрим на примере:

import asyncio
import functools


def event_handler(loop, stop=False):
    print('Вызван обработчик события')
    if stop:
        print('Цикл останавливается')
        loop.stop()


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    try:
        loop.call_soon(functools.partial(event_handler, loop))
        print('Цикл событий запускается')
        loop.call_soon(functools.partial(event_handler, loop, stop=True))

        loop.run_forever()
    finally:
        print('Завершаю цикл событий')
        loop.close()

Большая часть функцию ayncio не принимает аргументов, поэтому для передачи им параметров приходится ипользовать модуль functools. Наша основная функция в примере выше просто выводит в консоль некоторый текст. Если передать ей аргумент stop со значением True, она остановит цикл событий.

При первом вызове, мы не останавливаем цикл событий, но делаем это во втором вызове. Мы запустили цикл при помощи run_forever - что переводит его в режим бесконечного цикла, поэтому останавливать цикл нужно вручную. Как только цикл остановлен, его можно завершать. Если запустить написанный выше код, в консоли мы увидим следующий вывод:

Цикл событий запускается
Вызван обработчик события
Вызван обработчик события
Цикл останавливается
Завершаю цикл событий

Помимо call_soon, есть подобная функция call_soon_threadsafe. Как следует из названия, она выполняет те же действия, что и call_soon, но рассчитана на использование с потоками.

Если же вам необходимо отложить вызов функции до определенного момента в будущем, это можно сделать с использованием call_later. Для этого измените обращение к call_soon на следующий код:

loop.call_later(1, event_handler, loop)

Это отложит запуск обработчика на одну секунду, а затем выполнит его, передав объект цикла в качестве первого параметра.

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

current_time = loop.time()

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

loop.call_at(current_time + 300, event_handler, loop)

В этом примере мы используем текущее время, сохраненное ранее и добавляем к нему 300 секунд, то есть наши 5 минут. Таким образом, мы откладываем вызов обработчика на 5 минут, именно то, что нужно!

Задачи

Задачи (Tasks) наследуются от Future и являются оберткой над сопрограммами. Они позволяют следить за моментом, когда они завершат выполнение. Поскольку они имеют тип Future, остальные сопрограммы могут ожидать окончания задачи и использовать результат её выполнения. Давайте посмотрим на простой пример:

import asyncio
import time

async def my_task(seconds):
    """
    A task to do for a number of seconds
    """
    print('This task is taking {} seconds to complete'.format(
        seconds))
    time.sleep(seconds)
    return 'task finished'


if __name__ == '__main__':
    my_event_loop = asyncio.get_event_loop()
    try:
        print('task creation started')
        task_obj = my_event_loop.create_task(my_task(seconds=2))
        my_event_loop.run_until_complete(task_obj)
    finally:
        my_event_loop.close()

    print("The task's result was: {}".format(task_obj.result()))

Здесь мы создаем асинхронную функцию, которая принимает колиество секунд, которое определяет длительность выполнения функции. Таким образом мы симулируем долгоживущие процессы. Затем мы создаем цикл событий и объект задачи, вызывая метод цикла create_task. Create_task принимает в качестве параметра другую функцию, которую мы хотим преобразовать в задачу. После этого, мы указываем циклу событий продолжать выполнение, до окончания работы созданной задачи. В самом конце, мы обрабатываем результат задачи, который становится доступным сразу по её завершению.

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

Подводя итоги

К этому моменту, вам должно быть известно достаточно информации, чтобы самостоятельно начать работу с модулем asyncio. Эта библиотека предоставляет возможность выполнять много действительно интересных задач. Стоит посмотреть http://asyncio.org/, где публикуется проверенный список проектов, использующих asyncio, там можно почерпнуть много идей использования этой библиотеки. Документация к python тоже неплохое место для начала.

Оригинал статьи на английском: Python 3 – An Intro to asyncio