我什么时候应该使用asyncio.create_task?

7

我正在使用Python 3.10,对于asyncio.create_task有些困惑。

在下面的示例代码中,无论我是否使用asyncio.create_task,函数都会以协程的方式执行。似乎没有任何区别。

我应该在什么时候使用asyncio.create_task?相比不使用它,使用asyncio.create_task有什么优势呢?

import asyncio
from asyncio import sleep

async def process(index: int):
    await sleep(1)
    print('ok:', index)

async def main1():
    tasks = []
    for item in range(10):
        tasks.append(asyncio.create_task(process(item)))
    await asyncio.gather(*tasks)

async def main2():
    tasks = []
    for item in range(10):
        tasks.append(process(item)) # Without asyncio.create_task
    await asyncio.gather(*tasks)

asyncio.run(main1())
asyncio.run(main2())

2
create_task 函数提交协程以在“后台”中运行,即与当前任务和所有其他任务并发运行,在等待点之间进行切换。它返回一个可等待的句柄,称为“任务”,您还可以使用它来取消协程的执行。 - Cow
2
你没有看到任何不同,因为 asyncio.gather 已经将协程包装在任务中,因此自己创建任务并不会改变任何事情。 - user2357112
@user2357112 当我使用asyncio.gather时,添加asyncio.create_task是不必要的吗? - Vic
1
asyncio.gather:如果 aws 中的任何可等待对象是协程,它会自动作为任务调度。(请查阅文档) - Timus
@user2357112 这并不完全正确。创建任务确实会改变一些东西,特别是在gather调用之前引入了额外的上下文切换。在这个给定的例子中,确实没有区别,但通常情况下这种差异可能是巨大的。请参见下面我的答案以获取详细信息。 - Daniil Fajnberg
1个回答

9

简述

如果您想要立即安排协程的执行,但不一定要等待它完成,而是先转而处理其他事情,那么使用create_task是有意义的。

解释

正如评论中已经指出的那样,asyncio.gather本身会将提供的可等待对象包装成任务,因此在您的简单示例中预先调用create_task实际上是多余的。

gather文档中可以看到:

如果任何可等待对象[...]是协程,则会自动将其安排为任务。


话虽如此,但是你构建的这两个示例并不等价

当你调用create_task时,任务会立即在事件循环上安排执行。这意味着,如果在你为所有协程调用create_task后发生上下文切换(就像在第一个示例中一样),任何数量的协程都可能立即开始执行,而无需显式地await它们。

create_task文档中可以看到:(我强调)

将[...]协程包装成Task安排其执行

相比之下,当你仅仅创建协程(就像在第二个示例中一样),它们将不会自行开始执行,除非你以某种方式安排它们的执行(例如通过简单地await它们)。

如果您在创建协程和调用gather之间添加任何await(例如asyncio.sleep),并添加一些有用的print语句,您就可以看到它的实际效果:

from asyncio import create_task, gather, sleep, run


async def process(index: int):
    await sleep(.5)
    print('ok:', index)


async def main1():
    tasks = [create_task(process(item)) for item in range(5)]
    print("tasks scheduled")
    await sleep(2)
    print("now gathering tasks")
    await gather(*tasks)
    print("gathered in main1")


async def main2():
    coroutines = [process(item) for item in range(5)]
    print("coroutines created")
    await sleep(2)
    print("now gathering coroutines")
    await gather(*coroutines)
    print("gathered in main2")


run(main1())
run(main2())

输出:

tasks scheduled
ok: 0
ok: 1
ok: 2
ok: 3
ok: 4
now gathering tasks
gathered in main1

coroutines created
now gathering coroutines
ok: 0
ok: 1
ok: 2
ok: 3
ok: 4
gathered in main2

正如您所看到的,在main1示例中,process体在gather调用之前执行,而在main2示例中,它仅在之后执行。

因此,是否使用create_task取决于情况。如果您只关心协程在代码的特定点并发执行等待,那么调用create_task是没有用的。如果您想要安排它们,然后转而做其他事情,而它们可能或可能不会在后台完成任务,那么使用create_task是有意义的。

但是,需要记住的一件重要事情是,只有在某个时刻await它们,您才能确保您安排的任务实际上完全执行。这就是为什么您仍然应该await gather它们(或等效物)以最终等待它们完成的原因。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接