如何使用python的asyncio模块正确创建和运行并发任务?


76

我正在尝试Task使用Python 3的相对较新的asyncio模块正确理解和实现两个同时运行的对象。

简而言之,asyncio似乎旨在处理Task事件循环中的异步过程和并发执行。它促进了await(应用于异步函数中)作为无回调方式等待和使用结果的使用,而不会阻塞事件循环。(期货和回调仍然是可行的选择。)

它还提供了asyncio.Task()该类,该类是Future 设计用于包装协程的专门子类。最好通过使用该asyncio.ensure_future()方法来调用。异步任务的预期用途是允许独立运行的任务与同一事件循环中的其他任务“同时”运行。我的理解是Tasks连接到事件循环,然后该循环自动继续驱动await语句之间的协程。

我喜欢无需使用任何一个Executor类就可以使用并发Task的想法,但是在实现方面我没有发现太多细节。

这是我目前正在做的事情:

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表达式)以使事件循环处理多个任务是否正确?


4
是的,任务从原子自身执行yield from到明年yield from
安德鲁·斯维特洛夫

Answers:


79

是的,任何在事件循环中运行的协程将阻止其他协程和任务运行,除非它

  1. 使用yield fromawait(如果使用Python 3.5+)调用另一个协程。
  2. 退货。

这是因为asyncio是单线程的。事件循环运行的唯一方法是没有其他协程正在积极执行。使用yield from/await暂时暂停协程,使事件循环有工作的机会。

您的示例代码很好,但是在很多情况下,您可能不希望开始在事件循环内没有运行异步I / O的长时间运行的代码。在这些情况下,通常更适合asyncio.loop.run_in_executor在后台线程或进程中运行代码。ProcessPoolExecutor如果您的任务是CPU限制的,则是更好的选择;如果您ThreadPoolExecutor需要执行一些asyncio不友好的I / O ,则将使用它。

例如,您的两个循环完全受CPU限制,并且不共享任何状态,因此,最好的性能来自ProcessPoolExecutor于在CPU之间并行运行每个循环:

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.create_task(loop.run_in_executor(executor, say_boo))
    baa = asyncio.create_task(loop.run_in_executor(executor, say_baa))

    loop.run_forever()

谢谢。很好的时机,我只是想知道这个主题:使用执行程序。
songololo 2015年

尝试上面的代码,发现boo Task阻止了baa的运行,除非我将asyncio.sleep(0)的收益添加到while的每个True循环中?
songololo 2015年

还对run_in_executor位进行了如下修改:loop.run_in_executor(executor,asyncio.Task(say_boo()))
songololo 2015年

2
@shongololo对不起,已修复。asyncio.async应该使用而不是asyncio.Task直接使用构造函数。我们不想say_boosay_baa是协同程序,他们应该只是普通的功能,该事件循环的运行之外,所以你不应该添加yield from调用它们或在他们包裹asyncio.Task
dano 2015年

1
喜欢长相asyncio.async是一个别名ensure_future和现在已经过时
srobinson

14

您不必一定要yield from x控制事件循环。

在您的示例中,我认为正确的方法是执行ayield 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()

协程只是普通的旧Python生成器。在内部,asyncio事件循环会记录这些生成器,并gen.send()在永无休止的循环中逐一调用它们。无论何时yield,对的调用都会gen.send()完成,循环会继续进行。(我正在简化它;请查看https://hg.python.org/cpython/file/3.4/Lib/asyncio/tasks.py#l265以获得实际代码)

也就是说,run_in_executor如果您需要执行CPU密集型计算而不共享数据,我还是会走这条路。


在Python 3.4中有效,但在Python 3.5中似乎无效。3.5是否有类似的方法?(None似乎比asyncio.sleep()到处使用都更优雅...)
songololo

22
从Python 3.5开始,正确的方法是使用asyncio.sleep(0)请参阅此讨论。
Jashandeep Sohi
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.