有关python里的async def和await到底在等什么
`async def`可以定义一个调用时不会立即执行的函数,要让它真正运行,你需要1)使用 `await` ,对应下面方法1;或者2)把它放到事件循环中,对应下面方法2和3。例如:(例子如果过长可以先跳过,结合后面的解释阅读)
```python
import asyncio
import time
async def test(name, delay):
print(f"{name} 开始: {time.strftime('%X')}")
await asyncio.sleep(delay)
print(f"{name} 结束: {time.strftime('%X')}")
return name
async def demo_await_only():
"""方法1:直接await - 顺序执行"""
print("=== 直接await ===")
r = test("任务1", 2)
r1 = await test("任务1", 2) # 等2秒
r2 = await test("任务2", 1) # 再等1秒
# 总共3秒
async def demo_with_tasks():
"""方法2:创建任务 - 并发执行"""
print("=== 创建任务 ===")
# 两个任务都放入事件循环,立即开始
t1 = asyncio.create_task(test("任务1", 2))
t2 = asyncio.create_task(test("任务2", 1))
# await只是等待它们完成
r1 = await t1
r2 = await t2
# 总共2秒(最长的任务决定总时间)
async def demo_gather():
"""方法3:使用gather - 自动创建任务"""
print("=== 使用gather ===")
# gather会自动创建任务并放入事件循环
results = await asyncio.gather(
test("任务1", 2),
test("任务2", 1)
)
# 总共2秒
async def main():
start = time.time()
await demo_await_only()
print(f"耗时: {time.time()-start:.1f}秒\n")
start = time.time()
await demo_with_tasks()
print(f"耗时: {time.time()-start:.1f}秒\n")
start = time.time()
await demo_gather()
print(f"耗时: {time.time()-start:.1f}秒")
asyncio.run(main())
```
上面的代码展现了3个执行异步函数test的方法。方法1中,由于test函数是异步的,所以`r = test("任务1", 2)` 这一行只会创建一个协程(返回r的类型是一个协程对象),里面的任务并不会实际运行,要等到下面的`await test`才会开始运行,所以当不涉及事件循环时await可以理解为异步函数的开关;
方法2和3都是把任务1和2放进事件循环中,这样当其中一个任务到了await点,可以把这个任务挂起等着,同时事件循环继续运行其他任务直到await完成。任务之间没有优先级(方法2、3都是如此,执行任务的顺序和写代码时的顺序无关),实际的执行顺序取决于取决于任务的I/O等待时间、await点的位置等。
这里有一个问题,await为什么一定可以拿出来等着,万一它的异步函数是一个计算密集型操作怎么办?
从事件循环的层面看,方法1相当于每一行都把一个任务放进事件循环,运行完后拿出来,然后在下一行再把一个任务放进去,所以方法1是串行的;而2和3中的任务是并发的,注意并发不等于并行,并发的意思是在只有一个线程的情况下,当其中一个任务遇到await点,进行不占用CPU的操作(如I/O),事件循环就可以切换到另一个任务,直到这个await运行完毕,继续这个任务。所以这里可能形成一个多层嵌套的await,最内层的await可以让出CPU,这个await包在一个async def里,新的async def又被外面的await引用,层层循环,但最内层一定要有一个能让出CPU的操作,这样才能产生并发的效果。所以await的最内层一定要有让出CPU的操作。当然,最内层如果是计算密集型,也不会有await点了,因为计算密集型不需要暂停。
而2和3的区别在于控制的粒度,2可以单独控制、检查、取消一个任务,可以只拿一个任务的结果,可以指定先运行(先await)什么任务,而方法3只能等所有任务完成一起返回;但是方法3也有3的好处,如果不用单独控制代码更简洁,批量处理任务更方便,还有gather函数自带的`timeout` 选项控制超时(统计循环总时间更方便)。
还要注意,因为普通的def函数必须一刻不停地运行到return,而async def可以被暂停,所以**await只能放在async def的函数里**,所以方法1如果用await直接启动某个async def,外面还得套一层async def,这个async def(一般是main)可以用`asyncio.run(main())`直接启动。
另外asyncio的东西也是异步函数async def,所以和方法1同理,`asyncio.gather(...)`和`asyncio.sleep(delay)`前面也要加await启动执行。
更多推荐



所有评论(0)