Введение

Мы продолжаем цикл статей об асинхронном программировании с использованием сопрограмм в Python. В предыдущей статье мы рассмотрели реализацию сопрограмм при помощи генераторов в Python 2.5 и выше, в этой же познакомимся с той инфраструктурой, которая построена на основе них в Python 3.

 

Модуль asyncio Python


 

В Python 3.4 был включён модуль asyncio, который, на самом деле, был доступен в виде отдельного пакета на PyPI ещё для Python 3.3. Этот модуль предоставляет всю необходимую инфраструктуру для написания однопоточного конкурентного кода с использованием сопрограмм неблокирующего ввода-вывода, мультиплексирования ввода-вывода через сокеты и другие ресурсы, запуска сетевых клиентов и серверов и т.д.

Его возможности:

  • цикл событий с разными его реализациями, оптимизированными для различных операционных систем;
  • абстракции «транспорта» и «протокола» (подобные тем, что используются в фреймворке Twisted);
  • поддержка TCP, UDP, SSL, конвейеров UNIX-процессов, отложенных вызовов;
  • адаптированный для использования с циклом событий класс Future, который представляет ещё не вычисленный результат асинхронной функции;
  • сопрограммы и задачи, основанные на основе генераторов;
  • поддержка отмены Future и сопрограмм;
  • примитивы синхронизации для использования с сопрограммами;
  • возможность запуска задач в пуле потоков либо пуле процессов, что позволяет даже взаимодействовать с библиотеками, которые совершают блокирующий ввод-вывод.

В основе модуля asyncio лежит цикл событий (event loop). Он отвечает за:

  • создание задач из сопрограмм и Future и их выполнение;
  • регистрацию, исполнение и отмену отложенных вызовов;
  • создание клиентских и серверных транспортов для различных видов коммуникации;
  • запуск подпроцессов и связи их с транспортами для взаимодействия со внешним процессом;
  • делегирование медленных вызовов обычных функций пулу потоков или пулу процессов.

Сопрограммы в asyncio – это генераторы, которые отвечают определённым требованиям. Ко всем сопрограммам должен быть применён декоратор @asyncio.coroutine.

Действия, которые поддерживают сопрограммы asyncio:

  • result = yield from future – приостановка выполнения сопрограммы до получения future значения и присвоение этого значения переменной result (если future отменён, возникает исключение CancelledError);
  • result = yield from coroutine – ожидание завершения работы другой сопрограммы и получение её результата;
  • return result – возврат значения в сопрограмму, ожидающую данную;
  • raise exception – выброс исключения для обработки его ожидающей сопрограммой (если оно не обработано в текущей).

Рассмотрим пример двух сопрограмм, одна из которых вызывает другую, производящую какие-то затратные вычисления (на самом деле, для простоты примера она будет просто приостанавливать своё выполнение на одну секунду и возвращать квадрат числа).

 

import asyncio

 

@asyncio.coroutine

def time_consuming_computation(x):

    print('Computing {0} ** 2...'.format(x))

    yield from asyncio.sleep(1)

    return x ** 2

 

@asyncio.coroutine

def process_data(x):

    result = yield from time_consuming_computation(x)

    print('{0} ** 2 = {1}'.format(x, result))

 

if __name__ == '__main__':

    loop = asyncio.get_event_loop()

    loop.run_until_complete(process_data(238))

    loop.close()

 

Функция get_event_loop модуля asyncio возвращает объект цикла событий, и мы используем его метод run_until_complete для запуска сопрограммы.

Какое же в данном случае преимущество перед обыкновенными функциями? Во время работы сопрограммы asyncio.sleep выполнение программы не блокируется, и, если бы у нас были другие запланированные для выполнения задачи, они могли бы в это время выполняться в том же самом потоке.

 

Данные сопрограмм

 

Несмотря на то, что данные сопрограммы выглядят как обычный последовательный код, на самом деле отдельные их части исполняются асинхронно. На схеме выше показано, в каком порядке исполняется код из примера.

Можем переписать созданный ранее пример при помощи модуля asyncio.

 

import asyncio

import random

import time

 

@asyncio.coroutine

def consume():

    """Сопрограмма обработки данных"""

    running_sum = 0

    count = 0

    while True:

        data = yield from produce()

        running_sum += data

        count += 1

        print('Got data: {}\nTotal count: {}\nAverage: {}\n'.format(

            data, count, running_sum / count))

 

@asyncio.coroutine

def produce():

    """Сопрограмма выдачи данных."""

    yield from asyncio.sleep(0.5)

    data = random.randint(0, 100)

    return data

 

def main():

    loop = asyncio.get_event_loop()

    loop.run_until_complete(consume())

    loop.close()

 

if __name__ == '__main__':

    main()

 

Обратите внимание, что пришлось изменить логику его работы: теперь основной является сопрограмма consumer, а producer выдаёт одну порцию данных. Причиной этого является рассмотренные ранее ограничения, накладываемые на сопрограммы asyncio, которые логично следуют из основного предназначения данного модуля: совершение асинхронного ввода-вывода. В более реальном примере аналог сопрограммы producer мог бы, например, получать данные с внешнего сервера или базы данных.