Введение
Мы продолжаем цикл статей об асинхронном программировании с использованием сопрограмм в Python. В предыдущей статье мы рассмотрели реализацию сопрограмм при помощи генераторов в Python 2.5 и выше, в этой же познакомимся с той инфраструктурой, которая построена на основе них в Python 3.
В 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 мог бы, например, получать данные с внешнего сервера или базы данных.
Статьи по схожей тематике