1 为什么需要协程?
为什么现在越来越多的语言都开始支持协程?
一般来说, 一个线程栈大小为1MB, 如果都用多线程, 那么在高并发下, cpu大部分的时间都将用于切换线程上下文, 而且线程的切换是在内核态完成的, 会耗费额外的空间和时间.而且由于内存都分配给线程栈了, 将频繁地进行内存置换算法, 浪费了很多cpu时间片.
协程, 可以理解为一种在线程里跑的子线程, 它的默认栈空间很小 (比如go的协程栈默认大小为2KB).
当多个协程在一个线程上运行时, 协程间会切换着运行, 协程的切换完全在用户态完成, 而且时机由程序员来自行调度, 从而使得线程的并发量大大提升
不过协程只适用于IO密集型程序(大部分时间在等待), 对于计算密集型程序, 协程的优势并不大, 因为没有给它切换的时间, cpu大部分时间都在工作
2 如何定义异步函数?
# 普通函数定义
def add1(x):
print(x+1)
return x+1
# 异步函数的定义
async def add2(x):
print("in async fun add")
return x+2
async关键字定义的函数就是异步函数,
异步函数的实例化对象就是一个future
# add2(1)就是一个future
future = add2(1) # 一个future对象就是一个协程
注意: 我这里说的是 异步函数的实例化对象 就是一个协程, 你可能会理解为异步函数的调用, 但我认为不合理, 因为这个协程并不会因为这个"调用"而开始执行. 在实例化后,这个协程的状态是pending, 即将要发生的
3 如何切换异步函数?
await后面必须跟一个协程(future), 就可以阻塞当前协程, 切换到这个新协程里执行
你可以把await认为是启动协程的一种方式, 和普通函数调用的效果相同
import asyncio
async def fn2():
print("fn2")
async def fn1():
print("start fn1")
await fn2()
print("end fn1")
async def main():
print("start main")
await fn2()
await fn1()
print("end main")
if __name__ == '__main__':
loop = asyncio.get_event_loop() # 这个线程创建一个事件循环
loop.run_until_complete(main()) # 运行异步函数直到完成
执行结果为:
start main
fn2
start fn1
fn2
end fn1
end main
4 如何在一个线程内并发执行多个异步函数?
(1) 创建事件循环
一个普通的线程要能同时处理多个异步函数, 就要创建一个事件循环:
import asyncio
def main():
loop = asyncio.new_event_loop()
注: python3.7及以后不再使用事件循环的写法, 而是使用asyncio.run(), 但本质上是一样的, 只是它把事件循环封装在内部了, 个人还是比较喜欢用asyncio.new_event_loop(), 因为它代表的是协程的本质-事件循环
(2) 事件循环的机制
- 在事件循环中, 会执行所有任务(即异步函数)
- 但同一时间, 只有一个任务在执行
- 当一个任务中执行await后, 此任务被挂起, 事件循环执行下一个任务
(3) 代码实战
比如, 现实生活中的一个例子, 点完外卖, 之后玩游戏, 等着外卖送到, 如何用协程实现这样一个案例呢?
这里可能有人会问, 为什么要用asyncio.sleep, 而不用time.sleep呢?
因为, await后面一个要跟一个future(一个异步函数的实例化对象), 可是time.sleep并不是异步函数, 也就不支持协程间切换, 就没法实现并发, 只能串行
import asyncio
import time
async def play_game():
"""玩游戏"""
print('start play_game')
await asyncio.sleep(1)
print("play_game...")
await asyncio.sleep(1)
print("play_game...")
await asyncio.sleep(1)
print('end play_game')
return "游戏gg了"
async def dian_wai_mai():
"""点外卖"""
print("dian_wai_mai")
await asyncio.sleep(1)
print("wai_mai on the way...")
await asyncio.sleep(1)
print("wai_mai on the way...")
await asyncio.sleep(1)
print("wai_mai arrive")
return "外卖到了"
async def main():
print("start main")
future1 = dian_wai_mai()
future2 = play_game()
ret1 = await future1
ret2 = await future2
print(ret1, ret2)
print("end main")
if __name__ == '__main__':
t1 = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
t2 = time.time()
print('cost:', t2-t1)
上述代码的运行结果如下:
start main
dian_wai_mai
wai_mai on the way...
wai_mai on the way...
wai_mai arrive
start play_game
play_game...
play_game...
end play_game
外卖到了 游戏gg了
end main
cost: 6.007081508636475
总耗时6秒, 一直傻等着外卖送到, 才开始打游戏, 不仅游戏凉凉了, 外卖也凉了, 这显然不是我们想要的效果
为什么会这样呢? 用await后它不应该自动切到别的协程吗?
用await确实会切换协程, 但你事先没有告诉事件循环有哪些协程, 它不知道切换到哪个协程, 所以事件循环就会按顺序坚持执行完 外卖协程 再执行 打游戏协程
那怎么提前告诉事件循环有哪些协程呢?
用asyncio.gather(), 看代码(仅对main函数进行了修改)
async def main():
print("start main")
future1 = dian_wai_mai()
future2 = play_game()
ret1, ret2 = await asyncio.gather(future1, future2)
print(ret1, ret2)
print("end main")
再看看这次的执行结果:
start main
dian_wai_mai
start play_game
wai_mai on the way...
play_game...
wai_mai on the way...
play_game...
wai_mai arrive
end play_game
外卖到了 游戏gg了
end main
cost: 3.003592014312744
ok, 这次符合我们的预期了