Python协程asyncio模块的演变及高级用法
本文深入解析Python asyncio模块的演进历程,从3.4版本的@asyncio.coroutine到3.5+的async/await语法,再到3.7引入的asyncio.run。详细对比asyncio.wait与gather在任务创建、结果获取及顺序处理上的差异,并介绍回调函数、超时设置及自省方法等高级应用技巧。
Python协程及asyncio基础知识
协程(coroutine),亦称为微线程,是实现多任务处理的另一种机制。作为比线程更小的执行单元,协程通常运行在单进程和单线程环境中。由于协程自带CPU上下文,它可以通过简单的事件循环机制切换任务,其切换效率远高于由操作系统调度的进程和线程切换。
Python实现协程主要依赖两个库:asyncio和gevent。鉴于asyncio已纳入Python标准库,无需通过pip额外安装,这使其作为Python原生协程实现方式更为普及。本文仅聚焦于asyncio模块的介绍;若读者对gevent感兴趣,可另行查阅相关专题文章。
asyncio模块自Python 3.4版本起作为标准库引入,原生支持协程异步IO操作。其编程模型核心在于事件循环:通常先定义一个协程函数或任务,从asyncio模块获取事件循环(loop),随后将待执行的协程任务(或任务列表)提交至loop中运行,从而完成异步IO操作。
定义协程函数及执行方法的演变
在早期的Python 3.4版本中,协程函数需通过@asyncio.coroutine装饰器和yield from语法来实现,具体代码示例如下。
import asyncio
@asyncio.coroutine
def func1(i):
print("协程函数{}马上开始执行。".format(i))
yield from asyncio.sleep(2)
print("协程函数{}执行完毕!".format(i))
if __name__ == '__main__':
# 获取事件循环
loop = asyncio.get_event_loop()
# 执行协程任务
loop.run_until_complete(func1(1))
# 关闭事件循环
loop.close()
在此示例中,我们定义了一个名为func1的协程函数,并可通过asyncio.iscoroutinefunction进行验证。定义完成后,首先获取事件循环loop,利用其run_until_complete方法执行协程任务,最后关闭loop。
print(asyncio.iscoroutinefunction(func1(1))) # True
自Python 3.5版本起,引入了async/await语法来定义协程函数,代码如下所示。每个协程函数均以async关键字声明,以此与普通函数区分;对于耗时操作或函数调用,使用await关键字声明,表示在遇到等待时挂起当前任务,以便切换到其他任务执行。
import asyncio
# 这是一个协程函数
async def func1(i):
print("协程函数{}马上开始执行。".format(i))
await asyncio.sleep(2)
print("协程函数{}执行完毕!".format(i))
if __name__ == '__main__':
# 获取事件循环
loop = asyncio.get_event_loop()
# 执行协程任务
loop.run_until_complete(func1(1))
# 关闭事件循环
loop.close()
在Python 3.7版本之前,执行协程任务需分三步进行,代码相对冗余。Python 3.7提供了更简洁的asyncio.run方法,上述代码可简化为:
import asyncio
async def func1(i):
print(f"协程函数{i}马上开始执行。")
await asyncio.sleep(2)
print(f"协程函数{i}执行完毕!")
if __name__ == '__main__':
asyncio.run(func1(1))
注:自Python 3.6版本起,支持使用f-string进行字符串格式化,这相当于format函数的简化版本。
创建协程任务的演变
在前面的演示案例中,仅执行了单个协程任务(函数)。在实际应用中,通常先由协程函数创建协程任务,将其加入协程任务列表,最后统一交由事件循环执行。
根据协程函数创建协程任务有多种方法,其中最新的是Python 3.7版本提供的asyncio.create_task方法,示例如下:
# 方法1:使用ensure_future方法。future代表一个对象,未执行的任务。
task1 = asyncio.ensure_future(func1(1))
task2 = asyncio.ensure_future(func1(2))
# 方法2:使用loop.create_task方法
task1 = loop.create_task(func1(1))
task2 = loop.create_task(func1(2))
# 方法3:使用Python 3.7提供的asyncio.create_task方法
task1 = asyncio.create_task(func1(1))
task2 = asyncio.create_task(func1(2))
创建多个协程任务列表后,还需使用asyncio.wait方法收集这些协程任务,并交由事件循环处理执行。
import asyncio
async def func1(i):
print(f"协程函数{i}马上开始执行。")
await asyncio.sleep(2)
print(f"协程函数{i}执行完毕!")
async def main():
tasks = []
# 创建包含4个协程任务的列表
for i in range(1, 5):
tasks.append(asyncio.create_task(func1(i)))
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())
执行效果如下所示,你会发现4个协程任务并非按顺序执行。

对于收集多个协程任务,Python还提供了新的asyncio.gather方法,其作用与asyncio.wait类似,但功能更强大。如果列表中传入的不是由create_task方法创建的协程任务,它会自动将函数封装成协程任务,示例如下:
import asyncio
async def func1(i):
print(f"协程函数{i}马上开始执行。")
await asyncio.sleep(2)
print(f"协程函数{i}执行完毕!")
async def main():
tasks = []
for i in range(1, 5):
# 这里未由协程函数创建协程任务
tasks.append(func1(i))
# 注意这里*号。gather自动将函数列表封装成了协程任务。
await asyncio.gather(*tasks)
if __name__ == '__main__':
asyncio.run(main())
获取协程任务执行结果
是的,gather方法具备将普通函数封装成协程任务的能力,但这并非两者最主要的区别。两者更大的区别在于协程任务执行完毕后对返回结果的处理方式。通常,获取任务执行结果对程序至关重要,因此有必要花更多时间详细了解这两个方法的使用细节。
asyncio.wait会返回两个值:done和pending。done为已完成的协程任务列表,pending为超时未完成的协程任务集合,需通过task.result()方法获取每个协程任务返回的结果;而asyncio.gather直接返回所有已完成协程任务的result,无需进行额外调用或操作即可得到全部结果。
我们来看两个示例。现在修改我们的协程函数,通过return语句为其增加一个返回值。
通过asyncio.wait获取协程任务执行结果
import asyncio
async def func1(i):
print(f"协程函数{i}马上开始执行。")
await asyncio.sleep(2)
return i
async def main():
tasks = []
for i in range(1, 5):
tasks.append(asyncio.create_task(func1(i)))
# 获取任务执行结果。
done, pending = await asyncio.wait(tasks)
for task in done:
print(f"执行结果: {task.result()}")
if __name__ == '__main__':
asyncio.run(main())
执行结果如下所示。你可以看到协程任务执行结果并非按任务添加的顺序返回。

通过asyncio.gather获取协程任务执行结果
继续修改我们的代码:
#-*- coding:utf-8 -*-
import asyncio
async def func1(i):
print(f"协程函数{i}马上开始执行。")
await asyncio.sleep(2)
return i
async def main():
tasks = []
for i in range(1, 5):
tasks.append(func1(i))
results = await asyncio.gather(*tasks)
for result in results:
print(f"执行结果: {result}")
if __name__ == '__main__':
asyncio.run(main())
执行结果如下所示。协程任务执行结果与任务添加顺序完全一致。

现在你知道gather和wait方法的真正区别了吗?
- gather具有把普通协程函数包装成协程任务的能力,而wait没有。wait只能接收包装后的协程任务列表作为参数。
- 两者返回值不同,wait返回的是已完成和未完成任务的列表,而gather直接返回协程任务执行结果。
- gather返回的任务执行结果是有序的,而wait方法获取的结果是无序的。
asyncio高级使用方法
给任务添加回调函数
我们还可以给每个协程任务通过add_done_callback的方法为单个协程任务添加回调函数,示例如下:
#-*- coding:utf-8 -*-
import asyncio
async def func1(i):
print(f"协程函数{i}马上开始执行。")
await asyncio.sleep(2)
return i
# 回调函数
def callback(future):
print(f"执行结果:{future.result()}")
async def main():
tasks = []
for i in range(1, 5):
task = asyncio.create_task(func1(i))
# 注意这里,增加回调函数
task.add_done_callback(callback)
tasks.append(task)
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())
设置任务超时
许多协程任务耗时较长,当使用wait方法收集协程任务时,可通过timeout选项设置任务切换前单个任务的最大等待时间长度,示例如下:
# 获取任务执行结果,如下所示:
done,pending = await asyncio.wait(tasks, timeout=10)
自省
- asyncio.current_task:返回当前运行的Task实例,如果没有正在运行的任务则返回None。如果loop为None,则会使用get_running_loop()获取当前事件循环。
- asyncio.all_tasks:返回事件循环所运行的未完成Task对象的集合。
以上就是Python协程asyncio模块的演变及高级用法的详细内容,更多关于Python协程asyncio模块的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









