协程和Python 3.5中的future / task之间的区别?


99

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

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

之间有什么区别:

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

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

和:

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))

注意:该示例返回结果,但这不是问题的重点。如果返回值很重要,请使用gather()代替wait()

无论返回值如何,我都希望在上保持清晰ensure_future()wait(coros)并且wait(futures)都运行协程,那么何时以及为什么要包装协程ensure_future

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

为了获得额外的抵免额,如果我要批量处理电话,该怎么办?例如,我需要拨打some_remote_call(...)1000次,但我不想同时连接1000个连接而粉碎Web服务器/数据库/等。这对于线程或进程池是可行的,但是有没有办法做到这一点asyncio

Answers:


94

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

未来就像PromiseJavascript中的对象一样。它就像一个占位符,代表着将在未来实现的价值。在上述情况下,在等待网络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 /协程/期货的示例:

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()

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

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

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

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

当然,您可以在官方手册上找到更多详细信息:https : //docs.python.org/3/library/asyncio.html


3
我已经更新了我的问题,使它更清楚了-如果我不需要协程的结果,是否还需要使用ensure_future()?而且,如果我确实需要结果,就不能使用run_until_complete(gather(coros))吗?
knite

1
ensure_future安排协程在事件循环中执行。所以我会说是的,这是必需的。但是当然您也可以使用其他功能/方法来安排协程。是的,您可以使用gather()-但收集将一直等到所有响应都收集完毕。
masnun

5
@AbuAshrafMasnun @knite gatherwait实际上使用来包装给定的协程作为任务ensure_future(请参见此处此处的资源)。因此,ensure_future事先使用毫无意义,与获取结果无关。
文森特

8
@AbuAshrafMasnun @knite还有ensure_future一个loop参数,因此没有理由使用loop.create_taskover ensure_future。并且run_in_executor不能与协程一起使用,而应该使用信号量
文森特

2
@vincent还有一个理由使用create_taskensure_future,看文档。引用create_task() (added in Python 3.7) is the preferable way for spawning new tasks.
masi

24

简单的答案

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

一些代码示例

让我们先清除一些术语:

  • 协程功能,您所需要async def的;
  • 协程对象,当您“调用”协程函数时得到的内容;
  • 任务,一个包裹在协程对象上的对象在事件循环上运行。

案例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,将控制权交给事件循环

如果替换main函数,则会看到不同的结果:

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 task是在之前执行await

实际上,这里的重点是将控制权交还给循环,您可以asyncio.sleep(0)用来查看相同的结果。

引擎盖下

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

asyncio.waitasyncio.ensure_future并且asyncio.gather实际上loop.create_task直接或间接调用。

另请注意文档

回调按注册顺序调用。每个回调将仅被调用一次。


1
感谢您的明确解释!不得不说,这是一个非常糟糕的设计。高级API泄漏了低级抽象,这使API过于复杂。
鲍里斯·伯科夫

1
查看精心设计的古玩项目
ospider

很好的解释!我认为await task2可以澄清电话的影响。在这两个示例中,loop.create_task()调用是在事件循环上调度task2的对象。因此,在这两个ex中,您都可以删除,await task2并且task2仍将最终运行。在ex2中,行为将是相同的,因为await task2我相信只是调度已经完成的任务(不会再次运行),而在ex1中,行为将略有不同,因为task2直到main完成才执行。要查看差异,请在print("end of main")ex1主目录的末尾添加
Andrew


3

摘自BDFL [2013]

任务

  • 这是包裹在未来的协程
  • Task类是Future类的子类
  • 因此,它与工作的await呢!

  • 它与裸协程有何不同?
  • 无需等待就可以取得进步
    • 只要您等待其他事情,即
      • 等待 [something_else]

考虑到这一点,ensure_future将其作为创建任务的名称是有意义的,因为无论您是否等待它(只要您等待某事),都会计算Future的结果。这使事件循环可以在您等待其他事情时完成您的任务。请注意,Python 3.7 create_task确保未来的首选方法。

注意:出于现代性的考虑,我将Guido幻灯片中的“收益率从”更改为“等待”。

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.