引言
这篇文章是我阅读asyncio的官方文档,以及一些技术博客的学习总结。主要对python asyncio做一个总体的介绍。当你在第一次阅读asyncio的官方文档的时候你会看到大量晦涩难懂的单词和概念,阅读完这篇博客你会对许多名词有一定的理解,再去深入学习asyncio的官方文档,你会感到轻松很多,这是我写这篇文章的目的,在文章的最后我将贴出我觉得有意义的参考文章的地址。
async IO是一种并发编程技术,python的asynio指的是一个包的名称,它是一个代码库是python对async IO技术的实现。
async IO是一种单线程和单进程的设计。说到并发技术,你可能第一时间想到的是多进程和多线程,可单进程和但线程怎么实现并发呢?事实上async IO通过一个事件循环(event loop)作为一个大脑协调调度任务,它的灵魂在于并发调度任务,尽管只有一个线程,仍然能给人一种并发的感觉,当然本质上它不是真正并行。
想象有一个围棋大师与一众棋手车轮战
我们假设:
- 棋手总共20名
- 大师完成一步棋需要1秒
- 棋手完成一步棋需要10秒
- 完成一场比赛大师和棋手每人要走30步
- 同步版本:大师每次只和一个棋手下棋,完成比赛后再战下一位棋手,完成一场比赛需要 ( 1 + 10 ) ∗ 30 = 330 秒 (1+10)*30=330秒 (1+10)∗30=330秒,完成全部的20场比赛需要 330 ∗ 20 = 6600 秒 330*20=6600秒 330∗20=6600秒
- 异步版本:大师每次花费1秒钟下完一步棋之后马上切换到下一张桌子,而它的对手则在等待他回来的时间里完成自己的动作,这样大师在所有20场比赛中完成一步棋只需要 20 ∗ 1 = 20 秒 20*1=20秒 20∗1=20秒,一共30步棋,完成紫塞的总时长只有 20 ∗ 30 = 600 秒 20*30=600秒 20∗30=600秒
一、初次体验
我们通过简单的示例,观察不同程序的总运行时间来感受asyncio
import time
from datetime import datetime
def work1():
time.sleep(1)
print('work1已完成')
def work2():
time.sleep(1)
print('work2已完成')
def main():
work1()
work2()
print("开始",datetime.now().time())
main()
print("结束",datetime.now().time())
# 开始 18:14:51.164776
# work1已完成
# work2已完成
# 结束 18:14:53.179675
import asyncio
from datetime import datetime
async def work1():
await asyncio.sleep(1)
print('work1已完成')
async def work2():
await asyncio.sleep(1)
print('work2已完成')
async def main():
print("开始", datetime.now().time())
await asyncio.gather(work1(),work2())
print("结束", datetime.now().time())
asyncio.run(main())
# 开始 18:17:50.173980
# work1已完成
# work2已完成
# 结束 18:17:51.179613
- 不用担心遇到陌生的名词,再接下来的章节中我会一一解释
- 4行 8行 定义了两个work需要消耗1秒钟的时间来完成
- 17行 使用asyncio.run()启动了event_loop作为协调任务的大脑,并将main()包装成第一个task传入
- 14行 通过asyncio.gather把两个work包装为task交给大脑并行调度执行
二、什么是coroutine
coroutine的定义
coroutine包含两个概念
- coroutine function
- coroutine object
对于coroutine function 和 coroutine object 我们都称之为 coroutine。
1.什么是coroutine function?
首先由async def
定义的函数被称为coroutine function,如下第3行代码中的async def main ()
import asyncio
async def main():
print('hello')
await asyncio.sleep(1)
print('world')
#下面的代码什么也不会做
coro_obj = main()
asyncio.run(main())
2.什么是coroutine object?
在上面的例子中,与普通的函数不同是,第9行调用coroutine function什么也发生,coroutine function内部的语句也不会被执行,它只会默默地返回一个coroutine object。
如上所示,coro_obj = main()
并不会执行,main()
会返回一个coroutine object。
coroutine怎么样才能运行起来?
让coroutine运行起来方法大概分为两类
- await它
- 把它包装成task(什么是task下一章节会讲)
下面我将介绍几种运行coroutine的方法
1.asyncio.run()
import asyncio
async def main():
print('hello')
await asyncio.sleep(1)
print('world')
#下面的代码什么也不会做
coro_obj = main()
asyncio.run(main())
在前面的案例中我们使用asyncio.run(main())
运行了main()
,而main()
是一个coroutine。asyncio.run()做了两件事:
- 首先它启动了一个event_loop(事件循环)作为顶层的大脑接管程序的运行
- 接着他把传入coroutine包装成一个task
当一个coroutine被封装成一个task的时候,task会被交给事件循环也就是我们的大脑,大脑会尽快的寻找机会执行它。
2.await coroutine
await 也可以执行coroutine
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
print(f"started at {time.strftime('%X')}")
await say_after(1, 'hello')
await say_after(2, 'world')
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())
- 16行:首先是前面提到的
main()
被asyncio.run()
包装成task在事件循环中被执行。 - 11行:
main()
在其内部遇到了运行到第11行await say_after(1, 'hello')
,await告诉事件循环,say_after
可能是一个耗时任务 - 5行:进入
say_after()
果然遇到了await asyncio.sleep(delay)
,它告诉事件循环(event_loop),我这里需要等待一秒种,你现在可以去处理别的任务了
started at 17:13:52
hello
world
finished at 17:13:55
3.asyncio.create_task()
前面提到asyncio.run()
可以把coroutine包装成task
使用aysncio.create_task()
方法也可以把coroutine包装成一个task,task会被直接交给事件循环,让我们的大脑(事件循环)尽快安排它执行。
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
task1 = asyncio.create_task(
say_after(1, 'hello'))
task2 = asyncio.create_task(
say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
#等待两个task完成,task也可以await,后面会讲
await task1
await task2
print(f"finished at {time.strftime('%X')}")
- 6行 7行 :
asyncio.create_task()
各把一个cotoutine也就是这里的say_after()
包装成task - 这两各task直接会被交给事件循环安排执行
started at 17:14:32
hello
world
finished at 17:14:34
4.asyncio.gather()
asyncio.gather()
可以一次性将多个coroutine包装成task,一起交给事件循环并发调度
它的返回结果是一个awaitable object(后面会讲),我们可以await它的返回结果来收集这些task的执行结果
文章最开始的例子,就是使用的asyncio.gather()
import asyncio
from datetime import datetime
async def work1():
await asyncio.sleep(1)
print('work1已完成')
async def work2():
await asyncio.sleep(1)
print('work2已完成')
async def main():
print("开始", datetime.now().time())
await asyncio.gather(work1(),work2())
print("结束", datetime.now().time())
asyncio.run(main())
# 开始 18:17:50.173980
# work1已完成
# work2已完成
# 结束 18:17:51.179613
5.asyncio.TaskGroup()
python 3.11中的asyncio.TaskGroup
能帮助我们创建并管理一组task,它比asyncio.gather()
更可靠,具体参考asyncio的官方文档,这里我只做简单介绍
async def main():
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(
say_after(1, 'hello'))
task2 = tg.create_task(
say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
# The await is implicit when the context manager exits.
print(f"finished at {time.strftime('%X')}")
三、什么是Awaitable
Awaitable( awaitable object/ 可等待对象)
如果一个对象能使用await,那它就是一个awaitable object。
主要的awaitable object有三种 coroutine、Task、Future,下面将对这三种awaitable object逐一讲解。
1.coroutine
在python中coroutine是awaitable,因此我们可以让一个coroutineawait
(等待)其它coroutine完成。
import asyncio
async def nested():
return 42
async def main():
#这里什么也不会发生,因为 "nested()"是coroutine object
#我们说过coroutine需要被await
nested()
#这里我们await了它,才会输出 "42"
print(await nested())
asyncio.run(main())
2.Task
我们使用asyncio.create_task
可以把coroutine包装成task,同时task也是一个awaitable。
import asyncio
async def nested():
return 42
async def main():
#被包装成了task的nested()很快就会被按安排运行
task = asyncio.create_task(nested())
#我们这里拿着task的引用可以取消task也可以等待它完成
await task
asyncio.run(main())
3.Future
Future是一种低级(low level)的Awaitable,它表示异步操作的最终结果
在一个coroutine中await
一个Future意味着,我们的coroutine要等待Future完成
task实际上是Future的一个子类哦,但在应用层面我们不需要自己创建,也不需要过多的关注Future。
三、Task的创建和使用
经过前面的学习我们知道task可以被事件循环直接安排执行,同时事件循环会帮助我们并发调度多个task,因此创建task是我们在进行asyncio并发编程过程中非常常用的操作,下面我们来回顾一下我们创建task的三种方法
asyncio.create_task()
asyncio.gather()
asyncio.TaskGroup.create_task()
在这里我只是带你了解这些方法的概念和基础用法,更多的可选参数以及细节,还是希望你能够去学习和参考python asyncio的官方文档。
接下来我将介绍的是几种task的使用技巧
1.如何保护长期task?
如果你想创建一个长期运行的后台task,维持好对task的引用非常重要,事件循环(event loop)只持有对task的弱引用,为了避免我们的长期任务被回收,需要将它们保存在集合中。
background_tasks = set()
for i in range(10):
task = asyncio.create_task(some_coro(param=i))
# Add task to the set. This creates a strong reference.
background_tasks.add(task)
# To prevent keeping references to finished tasks forever,
# make each task remove its own reference from the set after
# completion:
task.add_done_callback(background_tasks.discard)
2.task的取消与清理工作
使用task的引用我们可以轻松的取消task,当一个task被取消的时候将会抛出asyncio.CancelledError
,我们在coroutine function内部增加try/finally来捕获它并且通过finally来执行一些清理工作是很有必要的
import asyncio
async def my_coroutine():
try:
# 假设这是一些长时间运行的操作
await asyncio.sleep(10)
except asyncio.CancelledError:
# 可以在这里处理取消事件,但通常你会让异常继续传播
raise
finally:
# 这里是清理代码,无论是否取消都会执行
print("Clean-up!")
async def main():
# 创建任务
task = asyncio.create_task(my_coroutine())
# 给任务一点时间去启动
await asyncio.sleep(1)
# 取消任务
task.cancel()
try:
# 等待任务完成,这里会抛出 asyncio.CancelledError
await task
except asyncio.CancelledError:
print("Task was cancelled")
asyncio.run(main())
四、asyncio设计模式
下面介绍两种asyncio程序的设计模式
Chaining Corotunies
我们可以将corotunie链接起来组合成一个任务链,链上的任务自上而下形成层级关系,同时运行多条任务链又不会导致堵塞,下事件循环的指挥下见缝插针并且井然有序的完成各自的工作。
#!/usr/bin/env python3
# chained.py
import asyncio
import random
import time
async def part1(n: int) -> str:
i = random.randint(0, 10)
print(f"part1({n}) sleeping for {i} seconds.")
await asyncio.sleep(i)
result = f"result{n}-1"
print(f"Returning part1({n}) == {result}.")
return result
async def part2(n: int, arg: str) -> str:
i = random.randint(0, 10)
print(f"part2{n, arg} sleeping for {i} seconds.")
await asyncio.sleep(i)
result = f"result{n}-2 derived from {arg}"
print(f"Returning part2{n, arg} == {result}.") return result
async def chain(n: int) -> None:
start = time.perf_counter()
p1 = await part1(n)
p2 = await part2(n, p1)
end = time.perf_counter() - start
print(f"-->Chained result{n} => {p2} (took {end:0.2f} seconds).")
async def main(*args):
await asyncio.gather(*(chain(n) for n in args))
if __name__ == "__main__":
import sys
random.seed(444)
args = [1, 2, 3] if len(sys.argv) == 1 else map(int, sys.argv[1:])
start = time.perf_counter()
asyncio.run(main(*args))
end = time.perf_counter() - start
print(f"Program finished in {end:0.2f} seconds.")
- 8行:part1是一个耗时任务,需要消耗随机生成的一定事件后返回结果。
- 16行:part2也是一个消耗随机时间并返回结果的耗时任务,不过part2要依赖part1的返回结果,part1运行完毕,part2才可以开始。
- 24行:定义了一个任务链,part1返回结果给part2,定义了依赖关系。
- 31行:我们通过main函数来生成多个任务链,并把他们交给主时间循环。
Producer-Consumer
借助asyncio.Queue()
队列,我们可以构建一个生产者消费者模型。
import asyncio, random
async def rnd_sleep(t):
# sleep for T seconds on average
await asyncio.sleep(t * random.random() * 2)
async def producer(queue):
while True:
# produce a token and send it to a consumer
token = random.random()
print(f'produced {token}')
if token < .05:
break
await queue.put(token)
await rnd_sleep(.1)
async def consumer(queue):
while True:
token = await queue.get()
# process the token received from a producer
await rnd_sleep(.3)
queue.task_done()
print(f'consumed {token}')
async def main():
queue = asyncio.Queue()
# fire up the both producers and consumers
producers = [asyncio.create_task(producer(queue))
for _ in range(3)]
consumers = [asyncio.create_task(consumer(queue))
for _ in range(10)]
# with both producers and consumers running, wait for
# the producers to finish
await asyncio.gather(*producers)
print('---- done producing')
# wait for the remaining tasks to be processed
await queue.join()
# cancel the consumers, which are now idle
for c in consumers:
c.cancel()
asyncio.run(main())
五、结语
asyncio还有更多的用法和细节,比如asyncio.timeout
等等都没有介绍到,但是这篇文章的目的不是做一篇保姆级的详细教程。正如我开篇所说,我希望更借助这篇文章能够帮助大家了解asyncio的一些概念和基础用法,为大家深入学习asyncio的官方文档打下基础。
asyncio官方文档
https://docs.python.org/3/library/asyncio.html
realpython关于asyncio的博客
https://realpython.com/async-io-python/
asyncio生产者消费者模型的实现博客
https://stackoverflow.com/questions/52582685/using-asyncio-queue-for-producer-consumer-flow