带你搞懂python协程 (async await asyncio)

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

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, 这次符合我们的预期了

猜你喜欢

转载自juejin.im/post/7095400034165850148