在Python 3.5中,协程(coroutine)和future/task之间的区别是什么?

9 浏览
0 Comments

在Python 3.5中,协程(coroutine)和future/task之间的区别是什么?

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

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,运行一系列非阻塞操作的正确方式是什么?

如果额外加分的话,如果我想要批量调用呢?例如,我需要调用some_remote_call(...)1000次,但我不想通过1000个同时连接来压垮网络服务器/数据库等。使用线程或进程池可以实现,但是否有办法使用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,它是一个强大的第三方替代方案。

0
0 Comments

在Python 3.5中,引入了协程(coroutine)的概念。协程是一种轻量级的并发编程方式,可以在不使用多线程或多进程的情况下实现并发操作。然而,在使用协程时,有时候会遇到一些问题,比如如何批量处理协程对象或者future对象。

在一个评论中,Vincent提到了一个链接,链接到了asyncio库中的tasks.py文件中的一行代码。这行代码展示了wait()函数是如何将协程对象包装成future对象的。换句话说,我们确实需要一个future对象,而协程对象将会被自动转换成future对象。

有人提出了一个问题,即对于一个协程对象c,await c是否等价于await create_task(c)。这个问题的出现可能是由于对协程和future/task之间的区别不够清楚导致的。下面是对这个问题的解答:

在Python 3.5中,我们可以使用async/await关键字来定义协程函数。协程函数可以通过await关键字来暂停执行,并等待其他协程的执行结果。当协程函数执行到await语句时,它会挂起当前的执行,并返回一个awaitable对象。这个awaitable对象可以是一个协程对象,也可以是一个future对象。

在Python 3.5之前的版本中,我们需要使用yield from语句来暂停协程的执行,并等待其他协程的执行结果。这种方式相对繁琐,而且容易出错。而在Python 3.5中,使用await关键字可以更加简洁和直观地实现协程的编写和调用。

在Python 3.5中,我们可以使用asyncio库来实现协程的调度和管理。asyncio库提供了一些函数和类来处理协程和future/task对象。其中,create_task()函数可以将一个协程对象转换成一个task对象。一个task对象可以被调度器调度,并在适当的时候执行。在协程函数中使用await关键字来等待一个task对象的执行结果。

asyncio库还提供了wait()函数来批量处理协程对象或者future对象。wait()函数可以接收一个可迭代对象,并返回一个future对象的集合。这个集合中的每个future对象都代表一个协程对象或者future对象的执行结果。在使用wait()函数时,我们不需要手动将协程对象转换成future对象,因为wait()函数会自动帮我们完成这个过程。

所以,对于一个协程对象c,await c等价于await create_task(c)。在使用await关键字时,Python会自动将协程对象转换成task对象,并等待task对象的执行结果。

总之,Python 3.5中引入了协程的概念,使得并发编程更加简洁和直观。在使用协程时,我们可以使用asyncio库来实现协程的调度和管理。在处理协程对象或者future对象时,我们可以使用wait()函数来批量处理。同时,Python会自动将协程对象转换成task对象,并等待task对象的执行结果。这样,我们可以更加方便地编写和调用协程函数。

0
0 Comments

Python 3.5引入了协程(coroutine)的概念,用于异步编程。但是,这也引发了一些困惑,特别是与future/task的区别。本文将通过整理内容来解释这个问题的出现原因以及解决方法。

协程是一个特殊的函数,使用`async def`定义。调用一个协程函数不会直接运行它,而是返回一个协程对象,类似于生成器函数返回生成器对象。

`await`可以用于获取协程的返回值,实际上是“调用”协程。

`ensure_future`和`create_task`将一个协程封装起来,并在下一次迭代时安排它在事件循环上运行,但不会等待它完成,类似于守护线程。

通过`await`一个协程或一个封装了协程的任务,你总是可以获取到协程返回的结果,区别在于它们的执行顺序。

下面的代码示例展示了这些概念的用法和区别。

首先,我们定义了一个协程函数`log_time`,用于打印当前时间和指定的词语。

import asyncio
import time
# 协程函数
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'))  # <- 在下次迭代中运行
    await coro  # <-- 直接运行
    await task
if __name__ == "__main__":
    asyncio.run(main())

运行结果如下,协程`coro`首先执行,然后是`task`:

1539486251.7055213 - plain await
1539486251.7055705 - create_task

在第二个示例中,我们调用`asyncio.sleep(1)`,将控制权交还给事件循环。预期结果将会有所不同。

async def main():
    coro = log_time('plain await')
    task = asyncio.create_task(log_time('create_task'))  # <- 在下次迭代中运行
    await asyncio.sleep(1)  # <- 事件循环获取控制权,并运行task
    await coro  # <-- 直接运行
    await task

运行结果如下,执行顺序被颠倒了:

1539486378.5244057 - create_task
1539486379.5252144 - plain await

调用`asyncio.sleep(1)`将控制权交还给事件循环,循环检查要运行的任务,然后先运行由`create_task`创建的`task`。尽管我们首先调用了协程函数,但没有使用`await`,我们只是创建了一个协程,它不会自动启动。然后,我们创建了一个新的协程并用`create_task`封装它,`create_task`不仅封装了协程,还将任务安排在下一次迭代中运行。结果中,`create_task`在`plain await`之前执行。

通过上述例子可以看出,无论是`await`一个协程还是一个封装了协程的任务,都可以获取到它们返回的结果,区别在于它们的执行顺序。

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

`asyncio.wait`、`asyncio.ensure_future`和`asyncio.gather`实际上直接或间接地调用了`loop.create_task`。

需要注意的是,在[官方文档](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.call_soon)中有这样的说明:

> 回调按注册的顺序调用,每个回调只会被调用一次。

总结一下,如果你`await`一个协程或一个封装了协程的任务(即可等待对象),你总是可以获取到它们返回的结果。这是协程和future/task的关键区别。

虽然Python 3.5的协程和future/task的概念有些令人困惑,但理解它们的区别和使用方法,可以帮助我们更好地进行异步编程。

以上就是Python 3.5中协程和future/task的区别以及解决方法。希望对你有所帮助!

0
0 Comments

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

Future对象类似于Javascript中的Promise对象。它是一个将来可能会实现的值的占位符。在上述情况中,当我们等待网络I/O时,函数可以给我们一个容器,一个承诺在操作完成时将填充容器的值。我们保留future对象,当它被实现时,我们可以调用其上的方法来检索实际的结果。

直接回答:如果你不需要结果,就不需要使用ensure_future。如果你需要结果或检索出现的异常,则使用它们很好。

额外加分:我会选择run_in_executor,并传递一个Executor实例来控制最大工作线程数。

在第一个示例中,你使用的是协程。wait()函数接受一组协程,并将它们组合在一起。所以当所有协程都用尽(完成/返回所有值)时,wait()结束。

run_until_complete方法会确保循环在执行完成前一直保持活动状态。请注意,在这种情况下,你没有获取异步执行结果。

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

所以在这个例子中,我们做的事情是一样的,只是我们使用了future而不是仅仅使用协程。

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

在这个例子中,我们使用了循环对象上的create_task方法。ensure_future会在主事件循环中安排任务。这种方法使我们能够在我们选择的循环中安排一个协程。

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

当协程返回一个值、引发异常或被取消时,任务就完成了。有一些方法可以检查这些情况。

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

- http://masnun.com/2015/11/13/python-generators-coroutines-native-coroutines-and-async-await.html

- http://masnun.com/2015/11/20/python-asyncio-future-task-and-the-event-loop.html

- http://masnun.com/2015/12/07/python-3-using-blocking-functions-or-codes-with-asyncio.html

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

我更新了我的问题,让它更加清晰一些 - 如果我不需要协程的结果,我还需要使用ensure_future吗?如果我需要结果,我不能只使用run_until_complete(gather(coros))吗?

如果你不需要结果,那么wait就可以正常工作,你不需要ensure_future。对不起,我误解了你的第一个评论。

然而,请注意,ensure_future会立即安排协程执行。所以在迭代每个项目时进行调度。使用wait时,你在收集协程后再进行调度。但这在这种情况下可能是一个细微的细节。

谢谢你的解释。也许你可以进一步完善你的答案?此外,关于批处理协程的“额外积分”有什么想法吗?

我已经更新了答案。还回答了其中的额外积分部分。

gather和wait实际上是使用ensure_future将给定的协程包装为任务(请参阅这里和这里的源代码)。因此,提前使用ensure_future没有意义,与是否获取结果无关。

你是对的。谢谢你指出来。我找不到wait的文档,所以根据记忆回答的。ensure_future允许一次创建一个任务,然后我们可以添加一个回调函数来在结果可用时检索结果。wait将对一组协程执行相同的操作,所以结果一起检索。这是我的观点。

此外,ensure_future有一个loop参数,所以使用loop.create_task而不使用ensure_future是没有道理的。而run_in_executor不能与协程一起使用,应该使用信号量。

你能展示一些使用信号量而不是run_in_executor的示例吗?因为读了信号量的文档后,我完全困惑了。

使用create_task而不使用ensure_future是有原因的,请参阅文档。引用:create_task()(在Python 3.7中添加)是生成新任务的首选方法。

这应该被标记为正确答案!做得好!

这个答案中的第一个段落是我听到的关于协程的最好用例/解释!

0