Python 3.5中协同程序和future/task的区别?

gv8xihay  于 2023-03-16  发布在  Python
关注(0)|答案(5)|浏览(166)

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

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)都运行协程,那么什么时候以及为什么要将协程 Package 在ensure_future中呢?
基本上,使用Python 3.5的async运行一系列非阻塞操作的正确方法是什么?
如果我想批处理调用,该怎么办?例如,我需要调用some_remote_call(...) 1000次,但我不想用1000个同时连接压垮web服务器/数据库/等。这可以用线程或进程池实现,但有没有办法用asyncio实现呢?

2020年更新(Python 3.7+):不要使用这些代码段,而用途:

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替代品。

5hcedyr0

5hcedyr01#

协程是一个生成器函数,它既可以生成值,也可以从外部接收值。使用协程的好处是我们可以暂停函数的执行,稍后再继续执行。在网络操作的情况下,在等待响应时暂停函数的执行是有意义的。我们可以利用这段时间运行其他函数。
future就像Javascript中的Promise对象,它就像一个占位符,代表一个值,这个值将在未来被具体化。在上面提到的例子中,在等待网络I/O时,一个函数可以给予我们一个容器,一个承诺,当操作完成时,它将用这个值填充这个容器。我们持有future对象,当它被实现时,我们可以调用它的一个方法来检索实际结果。

**直接回答:**如果您不需要结果,则不需要ensure_future。如果您需要结果或检索出现异常,则ensure_future非常有用。
**额外学分:**我会选择run_in_executor并传递一个Executor示例来控制最大工作线程数。

说明和样本代码

在第一个例子中,我们使用的是协程,wait函数接收一组协程并将它们组合在一起,所以wait()在所有协程都用完时结束(completed/finished返回所有值)。

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

run_until_complete方法将确保循环在执行完成之前是活动的。请注意,在这种情况下,您无法获得异步执行的结果。
在第二个示例中,您使用ensure_future函数 Package 协程并返回Task对象,该对象是Future的一种。当您调用ensure_future时,协程计划在主事件循环中执行。返回的future/task对象还没有值,但随着时间的推移,当网络操作完成时,未来对象将保存操作的结果。

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

所以在这个例子中,我们做了同样的事情,除了我们使用future而不是仅仅使用协程。
让我们看一个如何使用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方法添加回调的概念。
当协程返回一个值、引发异常或被取消时,Task就是done。有一些方法可以检查这些事件。
我写了一些博客文章,这些主题可能会有所帮助:

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

mv1qrgav

mv1qrgav2#

靶区;DR

  • 调用协程函数(async def)将不会运行它。它返回一个协程对象,就像生成器函数返回生成器对象一样。
  • await从协程中检索值,即“调用”协程。
  • eusure_future/create_task Package 一个协程,并安排它在下一次迭代的事件循环中运行,但不会等待它完成,它就像一个守护线程。
  • 通过等待一个协程或一个任务 Package 协程,您总是可以检索由协程返回的结果,区别在于它们的执行顺序。

一些代码示例

让我们先明确一些术语:

  • 协程函数,一个你async def s;
  • 协程对象,当你“调用”一个协程函数时你得到了什么;
  • task,一个围绕协同程序对象的对象,在事件循环上运行。
  • 可等待的,你可以await的东西,像任务,未来或普通的协程对象。

术语coroutine可以是协程函数,也可以是协程对象,这取决于上下文,但是您应该很容易区分它们。

案例1,协程上的await

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

import asyncio
import time

# coroutine function
async def log_time(word):
    print(f'{time.time()} - {word}')

async def main():
    coro = log_time('plain await')
    task = asyncio.create_task(log_time('create_task'))  # <- runs in next iteration
    await coro  # <-- run directly
    await task

if __name__ == "__main__":
    asyncio.run(main())

你会得到这样的结果,普通协程按预期首先执行:

1539486251.7055213 - plain await
1539486251.7055705 - create_task

因为coro是直接执行的,而task是在下一次迭代中执行的。

情况2,将控制权交给事件循环

通过调用asyncio.sleep(1),控制权被交还给循环,我们应该看到一个不同的结果:

async def main():
    coro = log_time('plain await')
    task = asyncio.create_task(log_time('create_task'))  # <- runs in next iteration
    await asyncio.sleep(1)  # <- loop got control, and runs task
    await coro  # <-- run directly
    await task

你会得到这样的结果,执行顺序是相反的:

1539486378.5244057 - create_task
1539486379.5252144 - plain await

调用asyncio.sleep(1)时,控制权被交还给事件循环,循环检查要运行的任务,然后首先运行由create_task创建的task
虽然我们首先调用了协程函数,但没有使用await,我们只是创建了一个协程,它不会自动启动。然后,我们创建了一个新的协程,并通过create_task调用将其 Package ,creat_task不仅 Package 了协程,还调度了任务在下一次迭代中运行。结果,create_taskplain await之前执行。
这里的神奇之处在于将控制权交还给循环,您可以使用asyncio.sleep(0)来实现相同的结果。
在所有的差异之后,同样的事情是:如果你等待一个协程或者一个 Package 协程的任务,也就是一个可等待的,你总是可以取回它们返回的结果。

引擎盖下

asyncio.create_task调用asyncio.tasks.Task()asyncio.tasks.Task()将调用loop.call_soonloop.call_soon将任务放入loop._ready。在循环的每次迭代期间,它检查loop._ready中的每个回调并运行它。
asyncio.waitasyncio.ensure_futureasyncio.gather实际上直接或间接地调用loop.create_task
在文件中还应注意:
回调函数是按照它们注册的顺序调用的。每个回调函数只调用一次。

krcsximq

krcsximq3#

Vincent的一条评论链接到https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346,它显示wait()为您将协程 Package 在ensure_future()中!
换句话说,我们确实需要一个未来,而协程将默默地转化为未来。
当我找到关于如何批处理协程/future的明确解释时,我会更新这个答案。

slsn1g29

slsn1g294#

From the BDFL [2013]

任务

  • 它是一个被Future包着的协同程序
  • 类Task是类Future的子类
  • 所以它也适用于wait
  • 它与裸协程有何不同?
  • 无需等待就能取得进展
  • 只要你等待别的东西,即.
    *等待[其他东西]

记住这一点,ensure_future作为创建Task的名字是有意义的,因为Future的结果将被计算,无论你是否等待它(只要你等待一些东西),这允许事件循环在你等待其他事情的时候完成你的Task,注意在Python 3.7中create_task是确保Future的首选方式。

  • 注意:我在Guido的幻灯片中将“屈服于”改为“等待”现代化。*
1yjd4xko

1yjd4xko5#

虽然已经有了一些非常有用的答案,但它们并没有涵盖所有的细微差别,特别是,公认的答案不再是正确的。
不应该wait与协程一起使用-为了与新版本的库兼容。
文件:
自3.8版起已弃用,将在3.11版中删除:不推荐直接向wait()传递协同程序对象。
文档中的另一个声明可能对深入理解有用。wait的结果是futures。如果你想检查你的协程是否在result中,你应该先用create_task将它 Package 到future中(因为它是比ensure_future更好的创建任务的方法)。
wait()自动将协同程序调度为Tasks,稍后返回(done,pending)集合中隐式创建的Task对象,因此下面的代码无法正常工作:

async def foo():
    return 42

coro = foo() 
done, pending = await asyncio.wait({coro})

if coro in done:
    # This branch will never be run!

下面是如何修复上面的代码片段:

return 42

task = asyncio.create_task(foo()) 
done, pending = await asyncio.wait({task})

if task in done:
    # Everything will work as expected now.

相关问题