当前位置:博客首页 > Python > 正文

6个栗子轻松理解asyncio协程

作者: Jarvan 分类: Python 发布时间: 2020-07-13 09:50 百度已收录

本文来自平哥的猿人学python公众号:理解Python asyncio的简洁方式,转载主要目的是我个人学习理解asyncio协程并发

栗子一:模拟一个异步IO(非并发)

我们定义了两个协程函数(在def前面加async),其中 hi() 我们把它叫做功能函数,通过一个 aysncio.sleep() 来模拟一个耗时的异步IO操作(比如下载网页), main() 叫做入口函数。其实就是在main() 里面调用 hi() 函数,通过不断改变 main() 的行为来理解异步IO(协程函数的调用)的运行过程,代码如下:

# coding:utf-8
import time
import asyncio

async def hi(msg, sec):
    print('enter hi(), {} @{}'.format(msg, time.strftime('%H:%M:%S')))
    await asyncio.sleep(sec)
    print('exit hi(), {} @{}'.format(msg, time.strftime('%H:%M:%S')))
    return sec

async def main():
    print('main() begin at {}'.format(time.strftime('%H:%M:%S')))
    for i in range(1, 5):
        await hi(i, i)
    print('main() end at {}'.format(time.strftime('%H:%M:%S')))

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

上面代码运行结果:

栗子二: 协程函数如何运行

hi() 是一个协程函数,直接调用它返回的是一个协程对象,并没有真正运行它。我们来仔细看看协程函数 hi() 的运行

# coding:utf-8
import time
import asyncio

async def hi(msg, sec):
    print('enter hi(), {} @{}'.format(msg, time.strftime('%H:%M:%S')))
    await asyncio.sleep(sec)
    print('exit hi(), {} @{}'.format(msg, time.strftime('%H:%M:%S')))
    return sec

async def main():
    print('main() begin at {}'.format(time.strftime('%H:%M:%S')))
    a = hi('a', 1)
    print('a is', a)
    b = await a
    print('b is', b)
    print('main() end at {}'.format(time.strftime('%H:%M:%S')))

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

我们像运行普通函数一样运行 hi() ,得到的a只是一个协程对象,见结果第二行:
a is: <coroutine object hi at 0x7fbf037f7050>

这个协程对象 a 虽然生成了,但是还没有运行,它需要一个时机。也就是asyncio的事件循环正在运行main,还没有空去运行它。

下面,我们通过 await 告诉 event_loop(事件循环) ,main协程停在这里,你去运行其它协程吧。这时候 event_loop 去执行a协程,也就是去执行 hi() 函数里面的代码。等 hi() 运行完,event_loop 再回到main协程继续从21行开始执行,把 hi() 的返回值赋值给b,这时候 b 的值是1。

event_loop 在整个异步IO过程中扮演一个管家的角色,在不同的协程之间切换运行代码,切换是通过事件来进行的,通过 await 离开当前协程,await 的协程完成后又回到之前的协程对应的地方继续执行。

栗子三: 协程函数异步非并发

异步IO的好处就是并发,但如何实现呢? 我们先来看一个异步非并发的例子(跟栗子一基本一致):

# coding:utf-8
import time
import asyncio

async def hi(msg, sec):
    print('enter hi(), {} @{}'.format(msg, time.strftime('%H:%M:%S')))
    await asyncio.sleep(sec)
    print('exit hi(), {} @{}'.format(msg, time.strftime('%H:%M:%S')))
    return sec

async def main():
    print('main() begin at {}'.format(time.strftime('%H:%M:%S')))
    for i in range(1, 5):
        b = await hi(i, i)
        print('b is:',b)
    print('main() end at {}'.format(time.strftime('%H:%M:%S')))

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

for 循环执行了4次,运行结果:

整个过程从09:20:54到 09:21:04结束,用了10秒。而hi()的执行时间分别是1秒,2秒,3秒,4秒总共10秒。也就是4个hi() 虽然是异步的但是顺序执行的,没有并发。

栗子四: 协程函数异步并发

接下来,就到了并发的实现了,通过 asyncio.creat_task() 即可:

# coding:utf-8
import time
import asyncio

async def hi(msg, sec):
    print('enter hi(), {} @{}'.format(msg, time.strftime('%H:%M:%S')))
    await asyncio.sleep(sec)
    print('exit hi(), {} @{}'.format(msg, time.strftime('%H:%M:%S')))
    return sec

async def main():
    print('main() begin at {}'.format(time.strftime('%H:%M:%S')))
    tasks = []
    for i in range(1, 5):
        t = asyncio.create_task(hi(i, i))
        tasks.append(t)
    
    for t in tasks:
        b = await t
        print('b is:', b)
        print('main() end at {}'.format(time.strftime('%H:%M:%S')))

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

代码运行结果:

通过 create_task() 我们在for循环里面生成了4个task(也是协程对象),但是这4个协程任务并没有被执行,它们需要等待一个时机:当前协程(main)遇到 await。

第二个for循环开始逐一 await 协程,此时 event_loop 就可以空出手来去执行那4个协程,过程大致如下:

  1.  先执行hi(1, 1) ,打印“enter hi(), 1 @21:58:35”,遇到await asyncio.sleep(1),当前协程挂起;
  2. 接着执行 hi(2, 2),执行打印命令,遇到await asyncio.sleep(2) ,当前协程挂起;
  3. 接着执行 hi(3, 3),执行打印命令,遇到await asyncio.sleep(3) ,当前协程挂起;
  4. 接着执行 hi(4, 4),执行打印命令,遇到await asyncio.sleep(4) ,当前协程挂起;
  5. 以上4步只是协程的切换和打印语句,执行非常快,我们可以认为它们是同时执行起来的。
  6. 1秒后,hi(1,1)的sleep结束它会发出事件告诉 event_loop 我await结束了,过来执行我,event_loop 此时空闲就来执行它,继续执行sleep后面的打印语句;
  7. 2秒后,hi(2,2)的sleep结束它会发出事件告诉 event_loop 我await结束了,过来执行我,event_loop 此时空闲就来执行它,继续执行sleep后面的打印语句;
  8. 3秒后,hi(3,3)的sleep结束它会发出事件告诉 event_loop 我await结束了,过来执行我,event_loop 此时空闲就来执行它,继续执行sleep后面的打印语句;
  9. 4秒后,hi(4,4)的sleep结束它会发出事件告诉 event_loop 我await结束了,过来执行我,event_loop 此时空闲就来执行它,继续执行sleep后面的打印语句;
  10. 4秒后,生成的4个协程任务就都执行完毕。总耗时4秒,也就是我们的4个任务并发完成了。

根据上面讲述的执行流程,可以看到结果对应起来了。4个任务都是在18秒时开始执行,以后每个1秒完成一个。main函数从18执行到22介绍,共耗时4秒。

栗子五: 错误的协程并发案例

上面的并发很完美,但有时候你可能会犯错。比如下面的main(), 你可能只是并发 hi() 函数,但不需要它的返回结果,于是有了下面的 main():

# coding:utf-8
import time
import asyncio

async def hi(msg, sec):
    print('enter hi(), {} @{}'.format(msg, time.strftime('%H:%M:%S')))
    await asyncio.sleep(sec)
    print('exit hi(), {} @{}'.format(msg, time.strftime('%H:%M:%S')))
    return sec

async def main():
    print('main() begin at {}'.format(time.strftime('%H:%M:%S')))
    for i in range(1, 5):
        asyncio.create_task(hi(i, i))
    
    print('main() end at {}'.format(time.strftime('%H:%M:%S')))

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

main()的for循环只是生成了4个task协程,然后就退出了。event_loop 收到main退出的事件就空出来去执行了那4个协程,进去了但都碰到了sleep。

然后event_loop就空闲了。这时候run() 就收到了main() 执行完毕的事件,run() 就执行完了,最后执行print,整个程序就退出了。

从main退出到整个程序退出就是一瞬间的事情,那4个协程还在傻傻的睡着,不,是在睡梦中死去了。

栗子六: 错误的协程并发案例2

在main()中加一个sleep会出现什么结果:

# coding:utf-8
import time
import asyncio

async def hi(msg, sec):
    print('enter hi(), {} @{}'.format(msg, time.strftime('%H:%M:%S')))
    await asyncio.sleep(sec)
    print('exit hi(), {} @{}'.format(msg, time.strftime('%H:%M:%S')))
    return sec

async def main():
    print('main() begin at {}'.format(time.strftime('%H:%M:%S')))
    for i in range(1, 5):
        asyncio.create_task(hi(i, i))
    print('main() sleep at {}'.format(time.strftime('%H:%M:%S')))
    await asyncio.sleep(2)
    print('main() end at {}'.format(time.strftime('%H:%M:%S')))

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

在main()退出前,我们要先sleep 2秒,再来猜猜它的运行结果是什么? 如果你对上面没有sleep的过程搞清楚了,不难猜到正确的结果:

注意:main() 的退出和 hi(2, 2) 的退出顺序。简单讲,main() 先sleep 2秒,hi(2, 2) 后sleep两秒,所以main先退出。

理解了sleep(2) 的执行过程,那么你就可以知道 sleep(4) 和 sleep(5) 的结果了。如果没有自信的话,就自己改一下时间,运行看看结果。

最后:如何判断是否要把函数定义为协程函数?

定义一个协程函数很简单,在def前面加async即可。那么如何判断一个函数该不该定义为协程函数呢?记住这一个原则:

如果该函数是要进行IO操作(读写网络、读写文件、读写数据库等),就把它定义为协程函数,否则就是普通函数。

以上就是如何理解asyncio的方法,也就是如何使用asyncawait这两个关键字。