写在前面:
视频原文:Advanced asyncio: Solving Real-world Production Problems - PyCon 2019
slides 链接:https://speakerdeck.com/roguelynn/advanced-asyncio-solving-real-world-production-problems
asyncio 初学者可能较难理解,可以结合我前面的几篇文章一起食用:
0x01 本文简介
原视频为 PyCon2019上的一场技术分享,作者是 Spotify的工程师, 通过一个案例, 介绍了 asyncio 的一些 best practice。
0x02 初始化 setup
concurrently publish messages
并发地 publish message:注意图中高亮的那一段,这里用的不是await queue.put(msg)
,这是因为await
会阻塞while
循环(参考前面的一篇文章:异步编程 101:asyncio中的 for 循环),也就是说,要等queue.put(msg)
完成了,下一趟才会开始。而asyncio.create_task()
会马上 "fire" 并且立即返回,你可以理解为fire and forget machinism。
如果你没时间看异步编程 101:asyncio中的 for 循环,我简要回顾一下:
await
做的事情是把当前的协程挂起,把控制权交给事件循环,以便于事件循环有其他协程可以调度时,接着运行其他协程。但是对于执行await
的这个协程而言,它是被阻塞的。这个例子中,publish()
中的while
循环是一个整体。
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/afa1c50f5b50a9ca99cdb074f4e23ea6.jpeg)
concurrently consume messages
这里使用msg = await queue.get()
是make sense 的,因为你得先得到 message 然后才能接着做其他事情。而后面的restart_host
则用create_task
,因为我们不想对他await
(等待它完成)而阻塞了整个 while Ture
循环。
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/e0f98bf022df0d59b5b3eafdd7565ba6.jpeg)
concurrent work
收到 message 之后,除了restart_host()
之外,我们可能还需要做一些其他的任务,比如持久化保存message
。
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/4c19b9aab3964155398df655d93d1a6d.jpeg)
这只需要在consume
方法里面再添加一个create_task()
:
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/2025d490a86d9984182991ff0b8d4d48.jpeg)
block when needed
然而有时候我们是希望异步任务能够serial
执行的。如果要把restart_host()
的逻辑改一下:先获取上次重启时间,然后判断上次重启时间是不是大于7天,如果是,再 restart_host()
。这里的last_restart_date()
和restart_host()
是有明确先后顺序的。
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/92047b2b1617c2a667e3fc4b64a012e1.jpeg)
但是我们又不想这里的线性执行影响后面的 message 获取,很简单,只要把这个逻辑封装成一个协程,然后create_task()
就行。
0x03 cleanup
需要对message
ack
,这样producer
才不会重新发送。所以现在处理消息的逻辑如下:
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/ff71065f0597b331f0e86ed88aa74eec.jpeg)
需要保证:save
和restart_host
全部完成之前,才能cleanup
。
使用await
是能够 work 的,但是性能肯定不够。
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/1979707e05f86d852ea1616ad80a8d98.jpeg)
所以asyncio.gather()
就派上用场了:这里把save()
和restart_host()
两个协程交给gather
,并传给它一个callback
,等两个任务全部完成之后调用callback
函数,也就是cleanup()
。
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/29548a2f18daa0df63ffa92a019f0808.jpeg)
如果不想用 callback
,也可以直接await
gather
,这样的 code 更加 clean:
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/5765cb774cfbb4eab0c96cefee435436.jpeg)
最后把程序跑一下,图中不同颜色表示的是同一个 message:
获取 message 是没有阻塞的
save
和restart_host
全部结束之后,才ack
message。
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/6a32140900ce6a3701463352363c643d.jpeg)
0x04 graceful shutdowns
把publish
和consume
组合起来,得到最后的main()
。
![640?wx_fmt=png](https://i-blog.csdnimg.cn/blog_migrate/41977a275926e79695cb7bf6f9f6fc71.jpeg)
0x05 总结一下
Asynchronous != concurrent
不是说你在原来代码的基础上加上async
、await
就能获得并发性能的,很可能你的异步代码还是 sequantial 的。
Serial != blocking
一些有先后顺序的操作,不意味着就一定是要 block 的。比如先做A 在做 B,在等待 A 完成的过程中,我可以抽出时间做 C。放到本文的例子来说,我需要先save
和restart_host
才能cleanup
,但我可以在等待save
和restart_host
的时候继续做其他事情:把控制权交给主事件循环,接着运行其他协程。
回顾一下知识点
await
的作用是什么?asyncio.create_task()
asyncio.gather()
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/d04727e8c04ff66e0f89449c5667952b.jpeg)