Python中的协程与asyncio原理

Posted 小杰666

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python中的协程与asyncio原理相关的知识,希望对你有一定的参考价值。

Python协程与asyncio原理

直接看Python代码,下面有详细的注释:

# 研究asyncio与协程的原理,python版本3.8
# 以下仅从代码执行与调试过程来理解协程,并不一定与协程的真正实现一致
# Python用保存函数的栈帧来恢复暂停点的继续执行,从而实现协程

import asyncio
import datetime


async def hi(num, sec):
    print(f'enter hi(), #num @datetime.datetime.now()')

    # 遇到await会挂起,执行后面的协程,等asyncio.sleep协程执行完,
    # 通知事件循环从挂起状态恢复,并从此处继续往下执行
    await asyncio.sleep(sec)

    print(f'exit hi(), #num @datetime.datetime.now()')

    # 注意,返回(即显式的return或隐式返回)表示函数执行完了,函数退出了,
    # 而挂起表示函数执行过程中暂停了,下次可能还会回来,在暂停处恢复执行
    return f'#num, sec'


async def main():
    print(f'======== main() begin at datetime.datetime.now()')

    # 如果直接执行下面三行,而不创建task任务,将是异步阻塞,hi协程不会有并发效果
    # await hi(1, 2)
    # await hi(2, 2)
    # await hi(3, 2)

    tasks = []
    for i in range(1, 5):
        t = asyncio.create_task(hi(i, i))
        tasks.append(t)

    # 通过 asyncio.run(main()) 把事件循环跑起来,并在里面执行main协程:
    # 在main协程里创建task任务后,不管接下来的代码有没有await出现,任务列表里的所有协程一定会同时执行,
    # 🚩同时执行协程,其实是按顺序把hi协程执行起来,遇到await立刻挂起,接着执行下个协程,
    # 🚩注意,此时事件循环不会理会hi协程await后的sleep执行完成,并去恢复hi的挂起状态,
    # 🚩而是直到任务列表里的所有协程都跑起来了,才处理已经执行完的sleep,并恢复对应的hi协程。
    # 🚩也就是说,把任务列表里的所有协程跑起来的过程是同步的。

    # 因为任务列表里的第1个hi协程,sleep时间最短,是1秒,所以它先执行完成,
    # 事件循环会先恢复第1个hi协程的继续执行,以此类推。main协程里执行任务列表里的hi协程,分两种情况:
    # ① 如果没有出现await语句,main协程退出(return或引发异常)后,开始并发(同上🚩处所述)执行hi协程
    # ② 如果有await出现,main协程创建完任务后,在await的地方挂起,去执行hi协程(同上🚩处所述)

    # 上面两种情况的区别是:
    # 第①种,没有await,当main也执行结束,挂起的所有hi协程中,没有任何一个协程会被事件循环恢复执行,
    # 因此只输出 enter hi(),而不会输出任何一个 exit hi()
    # 第②种,有await,main协程在await处会挂起,事件循环才去理会执行完的sleep,
    # 并恢复对应的那个hi协程,接着输出 exit hi(),然后返回

    # 下面是具体情况:

    # 第一种
    # b = await tasks[0]  # await第1个协程,1秒后第1个协程返回(其余协程的sleep还未结束),输出第一个exit hi()
    # print('b is:', b)  # b结果为第1个协程(即tasks[0])的返回值,即返回1

    # 第二种
    # b = await tasks[1]  # await第2个协程,2秒后第2个协程返回(此时第1个协程也已返回),输出两个exit hi()
    # print('b is:', b)  # b结果为第2个协程(即tasks[1])的返回值,即返回2

    # 第三种
    # b = await tasks[3]  # await第4个协程,4秒后第4个协程返回(前三个协程也已返回),输出全部四个exit hi()
    # print('b is:', b)  # b结果为第4个协程(即tasks[3])的返回值,即返回4

    # 第四种
    # for t in tasks:
        # 下面一行,先挂起,并把任务列表里的所有协程都执行起来,如上文所述情况②,然后等hi协程恢复并返回,并把返回值给b
        # b = await t
        # print('b is:', b)

    print(f'======== main() sleep at datetime.datetime.now()')
    await asyncio.sleep(3)  # 这里,先执行asyncio.sleep(3),挂起main协程,然后把任务列表里的所有协程都执行起来
    print(f'======== main() end at datetime.datetime.now()')

    # 最后一种
    # 如果整个main协程里没有await,在main退出后,即使把任务列表里的所有协程执行起来了,
    # 也无法让某个hi协程里的await在sleep结束后,被事件循环恢复执行,
    # 因为恢复链路是断掉的,asyncio.run启动事件循环(event_loop)后,从main协程到hi协程,到asyncio.sleep协程,
    # 这整条协程链路是通的,才能被事件循环从main协程去唤起asyncio.sleep,并从hi一直返回回来,回到main协程


if __name__ == '__main__':
    asyncio.run(main())
    print('done')

运行结果:

======== main() begin at 2022-08-28 11:06:06.479365
======== main() sleep at 2022-08-28 11:06:06.479490
enter hi(), #1 @2022-08-28 11:06:06.479547
enter hi(), #2 @2022-08-28 11:06:06.479569
enter hi(), #3 @2022-08-28 11:06:06.479584
enter hi(), #4 @2022-08-28 11:06:06.479597
exit hi(), #1 @2022-08-28 11:06:07.480400
exit hi(), #2 @2022-08-28 11:06:08.480724
======== main() end at 2022-08-28 11:06:09.479725
exit hi(), #3 @2022-08-28 11:06:09.479774
done

什么时候用协程函数呢?

如果要进行IO操作(读写网络、读写文件、读写数据库等),就把它定义为协程函数。
如果要进行计算密集型操作,推荐用多进程,因为有GIL。

参考:
https://www.yuanrenxue.com/tricks/python-asyncio-usage-exmaple.html

以上是关于Python中的协程与asyncio原理的主要内容,如果未能解决你的问题,请参考以下文章

Python中的协程与asyncio原理

2020-08-20:GO语言中的协程与Python中的协程的区别?

协程与异步IO

深究Python中的asyncio库-asyncio简介与关键字

Python协程之asyncio

Python协程与JavaScript协程的对比