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

时间:2015-03-26 01:08:22

标签: python concurrency task python-3.4 python-asyncio

我正在尝试使用Python 3相对较新的Task模块正确理解并实现两个并发运行的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表达式),以便事件循环处理多个任务?

2 个答案:

答案 0 :(得分:73)

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

  1. 使用yield fromawait调用另一个协同程序(如果使用Python 3.5 +)。
  2. 返回。
  3. 这是因为asyncio是单线程的;事件循环运行的唯一方法是没有其他协同程序正在执行。使用yield from / await暂时暂停协程,为事件循环提供工作机会。

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

    例如,你的两个循环完全受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.ensure_future(loop.run_in_executor(executor, say_boo))
        baa = asyncio.ensure_future(loop.run_in_executor(executor, say_baa))
    
        loop.run_forever()
    

答案 1 :(得分:13)

您不一定需要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的实际代码)

也就是说,如果你需要在没有共享数据的情况下进行CPU密集型计算,我仍然可以使用run_in_executor路由。