【问题标题】:Difference between coroutine and future/task in Python 3.5?Python 3.5 中协程和未来/任务之间的区别?
【发布时间】:2016-04-17 15:25:53
【问题描述】:

假设我们有一个虚拟函数:

async def foo(arg):
    result = await some_remote_call(arg)
    return result.upper()

两者有什么区别:

import asyncio    

coros = []
for i in range(5):
    coros.append(foo(i))

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(coros))

还有:

import asyncio

futures = []
for i in range(5):
    futures.append(asyncio.ensure_future(foo(i)))

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(futures))

注意:示例返回结果,但这不是问题的重点。当返回值很重要时,使用gather() 而不是wait()

无论返回值如何,我都在寻找ensure_future() 的清晰度。 wait(coros)wait(futures) 都运行协程,那么协程何时以及为什么应该包含在 ensure_future 中?

基本上,使用 Python 3.5 的 async 运行一堆非阻塞操作的正确方法 (tm) 是什么?

为了获得额外的信用,如果我想批量调用怎么办?例如,我需要调用some_remote_call(...) 1000 次,但我不想用 1000 个同时连接来破坏 Web 服务器/数据库/等。这对于线程或进程池是可行的,但是有没有办法使用asyncio 来做到这一点?

2020 年更新(Python 3.7+):不要使用这些 sn-ps。而是使用:

import asyncio

async def do_something_async():
    tasks = []
    for i in range(5):
        tasks.append(asyncio.create_task(foo(i)))
    await asyncio.gather(*tasks)

def do_something():
    asyncio.run(do_something_async)

还可以考虑使用 Trio,这是 asyncio 的强大的第 3 方替代方案。

【问题讨论】:

    标签: python python-3.x python-asyncio


    【解决方案1】:

    协程是一个生成器函数,它既可以产生值,也可以接受来自外部的值。使用协程的好处是我们可以暂停函数的执行并在以后恢复它。在网络操作的情况下,在我们等待响应时暂停函数的执行是有意义的。我们可以利用这段时间来运行一些其他的功能。

    未来就像 Javascript 中的 Promise 对象。它就像一个占位符,用于表示将来会实现的值。在上面提到的情况下,在等待网络 I/O 时,一个函数可以给我们一个容器,这是一个承诺,当操作完成时它将用该值填充容器。我们保留未来对象,当它完成时,我们可以调用它的方法来检索实际结果。

    直接回答:如果您不需要结果,则不需要ensure_future。如果您需要结果或检索发生的异常,它们很好。

    额外积分:我会选择 run_in_executor 并传递一个 Executor 实例来控制最大工作人员的数量。

    说明和示例代码

    在第一个示例中,您使用的是协程。 wait 函数需要一堆协程并将它们组合在一起。所以wait() 在所有协程都用尽时完成(完成/完成返回所有值)。

    loop = get_event_loop() # 
    loop.run_until_complete(wait(coros))
    

    run_until_complete 方法将确保循环在执行完成之前一直处于活动状态。请注意在这种情况下您如何没有获得异步执行的结果。

    在第二个示例中,您使用ensure_future 函数来包装协程并返回Task 对象,它是Future 的一种。当您调用ensure_future 时,协程计划在主事件循环中执行。返回的 future/task 对象还没有值,但随着时间的推移,当网络操作完成时,future 对象将保存操作的结果。

    from asyncio import ensure_future
    
    futures = []
    for i in range(5):
        futures.append(ensure_future(foo(i)))
    
    loop = get_event_loop()
    loop.run_until_complete(wait(futures))
    

    所以在这个例子中,我们在做同样的事情,除了我们使用期货而不是仅仅使用协程。

    我们来看一个如何使用 asyncio/coroutines/futures 的例子:

    import asyncio
    
    
    async def slow_operation():
        await asyncio.sleep(1)
        return 'Future is done!'
    
    
    def got_result(future):
        print(future.result())
    
        # We have result, so let's stop
        loop.stop()
    
    
    loop = asyncio.get_event_loop()
    task = loop.create_task(slow_operation())
    task.add_done_callback(got_result)
    
    # We run forever
    loop.run_forever()
    

    在这里,我们在loop 对象上使用了create_task 方法。 ensure_future 将在主事件循环中安排任务。这种方法使我们能够在我们选择的循环上调度协程。

    我们还看到了在任务对象上使用add_done_callback 方法添加回调的概念。

    当协程返回值、引发异常或被取消时,Taskdone。有一些方法可以检查这些事件。

    我写了一些关于这些主题的博客文章,可能会有所帮助:

    当然,你可以在官方手册中找到更多细节:https://docs.python.org/3/library/asyncio.html

    【讨论】:

    • 我已经更新了我的问题,使其更加清晰 - 如果我不需要协程的结果,我还需要使用 ensure_future() 吗?如果我确实需要结果,我不能只使用run_until_complete(gather(coros))吗?
    • ensure_future 调度协程在事件循环中执行。所以我会说是的,这是必需的。当然,您也可以使用其他函数/方法来安排协程。是的,您可以使用gather() - 但gather 会等到收集完所有回复。
    • @AbuAshrafMasnun @knite gatherwait 实际上使用 ensure_future 将给定的协程包装为任务(参见来源 herehere)。所以事先使用ensure_future是没有意义的,与是否得到结果无关。
    • @AbuAshrafMasnun @knite 另外,ensure_future 有一个loop 参数,因此没有理由使用loop.create_task 而非ensure_future。并且run_in_executor 不能与协程一起使用,应该使用semaphore
    • @vincent 有理由使用create_task 而不是ensure_future,请参阅docs。引用create_task() (added in Python 3.7) is the preferable way for spawning new tasks.
    【解决方案2】:

    简单回答

    • 调用协程函数 (async def) 不会运行它。它返回一个协程对象,就像生成器函数返回生成器对象一样。
    • await 从协程中检索值,即“调用”协程
    • eusure_future/create_task 安排协程在下一次迭代的事件循环上运行(虽然不等待它们完成,就像一个守护线程)。

    一些代码示例

    让我们先明确一些术语:

    • 协程函数,你async defs的那个;
    • 协程对象,当你“调用”一个协程函数时得到的;
    • task,一个包裹在协程对象上的对象,在事件循环中运行。

    案例 1,await 在协程上

    我们创建了两个协程,一个await,使用create_task运行另一个。

    import asyncio
    import time
    
    # coroutine function
    async def p(word):
        print(f'{time.time()} - {word}')
    
    
    async def main():
        loop = asyncio.get_event_loop()
        coro = p('await')  # coroutine
        task2 = loop.create_task(p('create_task'))  # <- runs in next iteration
        await coro  # <-- run directly
        await task2
    
    if __name__ == "__main__":
        loop = asyncio.get_event_loop()
        loop.run_until_complete(main())
    

    你会得到结果:

    1539486251.7055213 - await
    1539486251.7055705 - create_task
    

    解释:

    task1直接执行,task2在后面的迭代中执行。

    案例 2,让控制权交给事件循环

    如果我们替换主函数,我们可以看到不同的结果:

    async def main():
        loop = asyncio.get_event_loop()
        coro = p('await')
        task2 = loop.create_task(p('create_task'))  # scheduled to next iteration
        await asyncio.sleep(1)  # loop got control, and runs task2
        await coro  # run coro
        await task2
    

    你会得到结果:

    -> % python coro.py
    1539486378.5244057 - create_task
    1539486379.5252144 - await  # note the delay
    

    解释:

    当调用asyncio.sleep(1) 时,控制权交还给事件循环,循环检查要运行的任务,然后运行create_task 创建的任务。

    注意,我们首先调用协程函数,而不是await,所以我们只是创建了一个协程,并没有让它运行。然后,我们再次调用协程函数,并将其包装在 create_task 调用中,creat_task 实际上会安排协程在下一次迭代中运行。所以,在结果中,create taskawait 之前执行。

    其实这里的重点是把控制权交还给循环,你可以用asyncio.sleep(0)看到同样的结果。

    引擎盖下

    loop.create_task 实际上调用了asyncio.tasks.Task(),它会调用loop.call_soon。而loop.call_soon 会将任务放入loop._ready。在循环的每次迭代中,它会检查 loop._ready 中的每个回调并运行它。

    asyncio.waitasyncio.ensure_futureasyncio.gather实际上直接或间接调用loop.create_task

    还要注意docs:

    回调按照它们注册的顺序被调用。每个回调将被调用一次。

    【讨论】:

    • 感谢您的清晰解释!不得不说,这是一个非常糟糕的设计。高级 API 泄​​漏了低级抽象,这使 API 过于复杂。
    • 看看设计精良的古玩项目
    • 很好的解释!我认为await task2 调用的效果可以澄清。在这两个示例中,loop.create_task() 调用是在事件循环中调度 task2 的原因。因此,在这两个 exs 中,您都可以删除 await task2,但 task2 最终仍会运行。在 ex2 中,行为将是相同的,因为我相信 await task2 只是在调度已经完成的任务(不会第二次运行),而在 ex1 中,行为会略有不同,因为 task2 在 main 完成之前不会执行。要查看差异,请在 ex1 的 main 末尾添加 print("end of main")
    【解决方案3】:

    Vincent 的评论链接到https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346,这表明wait() 为您将协程包装在ensure_future() 中!

    换句话说,我们确实需要一个未来,协程会默默地转化为它们。

    当我找到关于如何批处理协程/期货的明确解释时,我会更新这个答案。

    【讨论】:

    • 是不是说对于协程对象cawait c等价于await create_task(c)
    【解决方案4】:

    From the BDFL [2013]

    任务

    • 这是一个封装在 Future 中的协程
    • Task 类是 Future 类的子类
    • 所以它也适用于 await

    • 它与裸协程有何不同?
    • 无需等待即可取得进展
      • 只要你等待别的东西,即
        • 等待 [something_else]

    考虑到这一点,ensure_future 作为创建任务的名称是有意义的,因为无论您是否等待(只要您等待某事),都会计算 Future 的结果。这允许事件循环在您等待其他事情时完成您的任务。请注意,在 Python 3.7 中,create_task 是首选方式 ensure a future

    注意:我在 Guido 的幻灯片中将“yield from”更改为“await”以实现现代性。

    【讨论】:

      猜你喜欢
      • 2010-12-15
      • 2010-12-28
      • 1970-01-01
      • 2011-04-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-16
      • 2015-05-08
      相关资源
      最近更新 更多