异步篇最接近Frodo的初衷了。通信与数据的内容使用传统框架的思路是相同的。而异步思路只改变了若干场景的实现方法。
异步编程不是新鲜概念,但他并没有指定很明确的技术特点和路线。相关概念也不是很清晰,很少有文章能细致地说明白 阻塞/非阻塞、异步/同步、并行/并发、分布式、IO多路复用、协程 这些概念的区别与联系。这些概念在CS专业的OS、分布式系统课程中可能有设计,但具体实现层面可能鲜有涉及。具体到Python这门语言,我阅读了很多工业界、python届的工作者(或者称为pythonista们)写的文章,下面两篇是最值得阅读的:
小白的 asyncio :原理、源码 到实现(1) - 闲谈后的文章 - 知乎; 当然标题是作者在自谦。该文作者结合CPython中asyncio标准源码、函数栈帧的源码和python函数上下文源码实现讲述了python异步的设计原理,并手写了一个简易版的事件循环和asyncio-future对象。
深入理解 Python 异步编程(上);这篇文章写于2017年,当时asyncio还没成为标准库。这篇文章大篇幅使用python和linux的epoll接口一步步实现了单线程异步IO,最后引出了asyncio的事件循环,证实了其便捷性。作者规划还有中下篇讲述asyncio的原理,可是目前还没等到下文。作者安放文章代码的仓库已经累计了数十条催更的issue。
基本问题
还记得我们再「通信篇」绘制的时序图吗?用它表示一次用户执行的逻辑是没问题的,但实际实现中,我们真的能这样写代码吗?这里有两个基本问题:
-
并发访问问题,如何实现多人同时访问你的博客web进程?
-
如何避免io阻塞,从而充分利用cpu的时间片?
第一个问题做过web开发的都很熟悉了,他的解决方案很多,因为这是软件发展中必须面对的问题:
-
os级别,io多路复用机制,成熟的为linux的epoll机制,
nginx
便是基于此实现访问并发。 -
编程语言使用多线程解决,以
Flask
为例,使用本地线程解决线程安全问题。 -
编程语言使用异步编程解决,以
nodejs
为例,promise
+回调的方式。python就是以asyncio
为代表的异步生态圈。
第二个问题其实跟第一个问题是一个意思,把对象换成cpu即可。Frodo
解决第一个问题使用的是类似asyncio事件循环的uvloop
循环,他包装成了一个机遇ASGI
协议的web服务器uvicorn
,他可以启动多个ASGI
标准写的app,内置一套事件循环实现并发访问。
uvicorn main:app --reload --host 0.0.0.0 --port 8001
重点是Frodo
对于第二个问题的解决,这些都是在程序细节中体现出的。
问题分析:哪里存在IO阻塞
我们拿「通信篇」中CRUD的通信逻辑举例,我们先标注出IO阻塞的地方, 然后对应到程序设计中的环节,再来思考在实现中怎么解决。
图中标注出了三类io场景,并有的是串行的需求,有的是并发(可以并发)的需求。我来分别解释下:
-
第一类: 网络的连接和断开,http是基于tcp的可靠传输协议,建立连接的过程也是耗时的io操作。数据库的连接是网络连接或套接字文件读写类的链接,也是io耗时的。这些代码主要在web中的checkpoin函数,在
Frodo
的views
目录下。 -
第二类: 通信异步是指客户端发送请求,等待数据准备好到返回的过程,这部分等到的时间其实是后端的数据io操作,cpu不应被这段时间占用。这部分代码在
Frodo
的mdoels
下。 -
第三类: 数据异步是指跟数据库操作等待数据返回所需的时间消耗。这部分时间也应该还给cpu。
上述的很多场景必须是串行完成的,比如建立数据库连接–>数据操作–>断开连接。也有一些场景(主要是不涉及数据一致性的场景)可以是并行的,如缓存的更新与删除,因为KV数据库不涉及关系的联立,可以并行地删除。
解决方案
第一类:连接耗时
数据库的连接与退出同步中都会想到使用带with
关键字的连接池,异步为了这一连接过程可以「被等待」或者说交出执行权给主程序,需要使用async
关键字包装一下,并实现异步上下文的方法__aenter__
, __aexit__
.
import databases
class AioDataBase():
async def __aenter__(self):
db = databases.Database(DB_URL.replace('+pymysql', ''))
await db.connect()
self.db = db
return db
async def __aexit__(self, exc_type, exc, tb)