Как правильно создавать и запускать параллельные задачи с помощью модуля 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 ответов
да, любая корутина, работающая внутри вашего цикла событий, блокирует запуск других сорутинов и задач, если только она
- сопрограмма вызывает другую, используя
yield from
илиawait
(при использовании Python 3.5+). - возвращает.
это так 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
маршрут, если вам нужно сделать интенсивное вычисление процессора без обмена данными.