Как правильно создавать и запускать параллельные задачи с помощью модуля asyncio python?

я пытаюсь правильно понять и реализовать два одновременно запущенных Task объекты, использующие Python 3 относительно новые asyncio модуль.

в двух словах, asyncio, кажется, предназначен для обработки асинхронных процессов и параллельных Task выполнение по циклу событий. Это способствует использованию await (применяется в асинхронных функциях) в качестве обратного вызова-бесплатный способ ждать и использовать результат, не блокируя цикл событий. (Фьючерс и обратные вызовы по-прежнему являются жизнеспособной альтернативой.)

он также предоставляет asyncio.Task() класс, специализированный подкласс Future предназначена для упаковки сопрограммы. Предпочтительно вызывается с помощью asyncio.ensure_future() метод. Предполагаемое использование задач asyncio-позволить независимо запущенным задачам работать "одновременно" с другими задачами в том же цикле событий. Насколько я понимаю, это Tasks подключены к циклу событий, который затем автоматически продолжает управлять сопрограммой между await заявления.

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

вот как я сейчас это делаю:

import asyncio

print('running async test')

async def say_boo():
    i = 0
    while True:
        await asyncio.sleep(0)
        print('...boo {0}'.format(i))
        i += 1

async def say_baa():
    i = 0
    while True:
        await asyncio.sleep(0)
        print('...baa {0}'.format(i))
        i += 1

# wrap in Task object
# -> automatically attaches to event loop and executes
boo = asyncio.ensure_future(say_boo())
baa = asyncio.ensure_future(say_baa())

loop = asyncio.get_event_loop()
loop.run_forever()

в случае попытки одновременного запуска двух задач цикла я заметил, что если задача не имеет внутреннего await выражение, оно застрянет в while петля, эффектно преграждая другое задачи от запуска (очень похоже на обычный while петли). Однако, как только задачи должны (a) ждать, они, похоже, выполняются одновременно без проблем.

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

пример вывода с внутренними await:

running async test
...boo 0
...baa 0
...boo 1
...baa 1
...boo 2
...baa 2

пример вывода без внутренние await:

...boo 0
...boo 1
...boo 2
...boo 3
...boo 4

вопросы

проходит ли эта реализация для "правильного" примера параллельных задач цикла в asyncio?

правильно ли, что это работает только для Task для обеспечения точки блокировки (await expression) для того, чтобы цикл событий жонглировал несколькими задачами?

2 ответов


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

  1. сопрограмма вызывает другую, используя yield from или await (при использовании Python 3.5+).
  2. возвращает.

это так asyncio является однопоточным; единственный способ запуска цикла событий - это активное выполнение другой подпрограммы. Используя yield from/await приостанавливает временно сопрограмма, давая цикл событий шанс работать.

ваш пример кода в порядке, но во многих случаях вы, вероятно, не хотели бы, чтобы длительный код, который не выполняет асинхронный ввод-вывод внутри цикла событий. В этих случаях часто имеет смысл использовать BaseEventLoop.run_in_executor для выполнения кода в фоновом потоке или процессе. ProcessPoolExecutor было бы лучшим выбором, если ваша задача связана с CPU,ThreadPoolExecutor будет использоваться, если вам нужно сделать некоторые операции ввода-вывода, не asyncio-дружелюбный.

ваши два цикла, например, полностью связаны с процессором и не разделяют никакого состояния, поэтому лучшая производительность будет получена от использования ProcessPoolExecutor для запуска каждого цикла параллельно через процессоры:

import asyncio
from concurrent.futures import ProcessPoolExecutor

print('running async test')

def say_boo():
    i = 0
    while True:
        print('...boo {0}'.format(i))
        i += 1


def say_baa():
    i = 0
    while True:
        print('...baa {0}'.format(i))
        i += 1

if __name__ == "__main__":
    executor = ProcessPoolExecutor(2)
    loop = asyncio.get_event_loop()
    boo = asyncio.ensure_future(loop.run_in_executor(executor, say_boo))
    baa = asyncio.ensure_future(loop.run_in_executor(executor, say_baa))

    loop.run_forever()

вам не обязательно нужен yield from x для управления циклом событий.

в вашем примере, я думаю правильный путь был бы сделать yield None или просто yield, а не yield from asyncio.sleep(0.001):

import asyncio

@asyncio.coroutine
def say_boo():
  i = 0
  while True:
    yield None
    print("...boo {0}".format(i))
    i += 1

@asyncio.coroutine
def say_baa():
  i = 0
  while True:
    yield
    print("...baa {0}".format(i))
    i += 1

boo_task = asyncio.async(say_boo())
baa_task = asyncio.async(say_baa())

loop = asyncio.get_event_loop()
loop.run_forever()

Coroutines - это просто старые генераторы Python. Внутри asyncio цикл событий ведет запись этих генераторов и вызовов gen.send() на каждой из них по одному в бесконечном цикле. Всякий раз, когда вы yield, в вызов gen.send() завершается и цикл может двигаться дальше. (Я упрощаю это; посмотрите вокруг https://hg.python.org/cpython/file/3.4/Lib/asyncio/tasks.py#l265 для фактического кода)

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