前面发表了《简说Python Web异步框架》一文后,很多朋友希望能更多了解Python关于asyncio的知识。正好,我也想深入理解理解这方面的知识,于是就促成了这篇文章。
一、软件系统的并发
使用异步IO,无非是提高我们写的软件系统的并发。这个软件系统,可以是网络爬虫,也可以是Web服务等等。
并发的方式有多种,多线程,多进程,异步IO等。多线程和多进程更多应用于CPU密集型的场景,比如科学计算的时间都耗费在CPU上,利用多核CPU来分担计算任务。多线程和多进程之间的场景切换和通讯代价很高,不适合IO密集型的场景(关于多线程和多进程的特点已经超出本文讨论的范畴,有兴趣的同学可以自行搜索深入理解)。而异步IO就是非常适合IO密集型的场景,比如网络爬虫和Web服务。
IO就是读写磁盘、读写网络的操作,这种读写速度比读写内存、CPU缓存慢得多,前者的耗时是后者的成千上万倍甚至更多。这就导致,IO密集型的场景99%以上的时间都花费在IO等待的时间上。异步IO就是把CPU从漫长的等待中解放出来的方法。
二、同步、异步与阻塞、非阻塞的区别
这是一个经久不衰的“问题”,各种解释和各种形象的比喻也很多,有兴趣的同学可以去知乎围观《怎样理解阻塞非阻塞与同步异步的区别?》这个问题。
我试着从另外的角度去解释一下这四个概念。这是两组不同维度的概念。
同步和异步:是方法论;
阻塞和非阻塞:是现象(结果)论。
处理IO,一种现象(结果)就是等IO处理完才去做别的,这就是阻塞;另一种现象是先发一个处理IO的命令就去做别的事情,等IO处理结束了再来对IO处理进行处理,这就是非阻塞。
阻塞是浪费时间的,所以我们要想办法解决阻塞使整个流程顺畅变成非阻塞。那么怎么办呢?
最一开始,IO只有最基本的同步方法,即阻塞的方法。阻塞不好,我们要非阻塞,于是,人们在同步IO上加了些东西(select/poll, epoll, kqueue)就得到了非阻塞的方法,这种方法就是IO复用(I/O Multiplexing),通常人们把这种IO复用也叫做异步IO。后来又实现了异步的API(aio_read等)。这就是操作系统IO发展的过程。参考下面这个表格,它来自Wikipedia的“异步IO”词条,说的是操作系统提供的IO操作的API。
我对这个表格的解释就是方法和现象。横向看是IO的两种方法:同步和异步。纵向看是IO的两种现象:阻塞和非阻塞。其中,同步的方法可以得到阻塞和非阻塞两种结果,而异步的方法只能得到非阻塞的结果。
举个例子,我们要做一顿午饭:淘米1分钟,蒸米饭20分钟,洗菜5分钟,切菜1分钟,炒菜20分钟。那么,做完这顿饭需要几分钟?不同的人需要的时间不一样。
笨的人,淘米、蒸米饭、洗菜、切菜、炒菜一步一步来,需要的时间最长。他的过程是阻塞的,方法是同步的,当然也是笨的。
聪明的人发现,用电饭煲蒸饭的同时,就可以去做菜了,这样完全节省了等待蒸米饭的那20分钟。他的过程变成了非阻塞的,方法变成了异步的。这个过程有个关键的东西:电饭煲,它只需要一个命令开关就去蒸饭了,蒸完饭会“滴”一声告诉你好了,其间不需要你操心,可以去洗菜、炒菜了。这就是异步实现的机制。
这里,“蒸饭”看做是耗时的IO过程,它的异步化就带来了整体效率的提高。
如果炒菜也有电器可以自动实现,把菜放进去,它洗、切、炒自动完成,熟了后也“滴”一声告诉你。那么你做饭的流程更加非阻塞化了,你做饭的方法也更加异步化。把材料放进电饭煲和炒菜机就可以去看电视或干点儿别的了。
三、Python的异步IO
异步IO的优势显而易见,各种语言都通过实现这个机制来提高自身的效率,Python也不例外。
(1)Python 2的异步IO库
Python 2 时代官方并没有异步IO的支持,但是有几个第三方库通过事件或事件循环(Event Loop)实现了异步IO,它们是:
twisted: 是事件驱动的网络库
gevent: greenlet + libevent(后来是libev或libuv)。通过协程(greenlet)和事件循环库(libev,libuv)实现的gevent使用很广泛。
tornado: 支持异步IO的web框架。自己实现了IOLOOP。
(2)Python 3 官方的异步IO
Python 3.4 加入了asyncio库,使得Python有了支持异步IO的官方库。这个库,底层是事件循环(EventLoop),上层是协程和任务。asyncio自从3.4 版本加入到最新的 3.7版一直在改进中。
Python 3.4 刚开始的asyncio的协程还是基于生成器的,通过yield from语法实现,可以通过装饰器@asyncio.coroutine(已过时)装饰一个函数来定义一个协程。比如:
Python 3.5 引入了两个新的关键字await和async用来替换@asyncio.coroutine和yield from,从语言本身来支持异步IO。从而使得异步编程更加简洁,并和普通的生成器区别开来。
注意:对基于生成器的协程的支持已弃用,并计划在 Python 3.10 中移除。所以,写异步IO程序时只需使用async和await即可。
Python 3.7 又进行了优化,把API分组为高层级API和低层级API。我们先看看下面的代码,发现与上面的有什么不同?
除了用 async 替换 @asyncio.coroutine 和用 await 替换 yield from 外,最大的变化就是关于eventloop的代码不见了,只有一个async.run()。这就是 3.7 的改进,把eventloop相关的API归入到低层级API,新引进run()作为高层级API让写应用程序的开发者调用,而不用再关心eventloop。除非你要写异步库(比如MySQL异步库)才会和eventloop打交道。
需要注意的是,async.run()是3.7版新增加的,处于暂定API状态。 暂定API,是指被有意排除在标准库的向后兼容性保证之外的应用编程接口。虽然此类接口通常不会再有重大改变,但只要其被标记为暂定,就可能在核心开发者确定有必要的情况下进行向后不兼容的更改(甚至包括移除该接口)。此种更改并不会随意进行 -- 仅在 API 被加入之前未考虑到的严重基础性缺陷被发现时才可能会这样做。即便是对暂定 API 来说,向后不兼容的更改也会被视为“最后的解决方案” —— 任何问题被确认时都会尽可能先尝试找到一种向后兼容的解决方案。这种处理过程允许标准库持续不断地演进,不至于被有问题的长期性设计缺陷所困。
从上面关于 asyncio 的发展来看它一直在变化,3.4,3.5,3.6, 3.7 都有很多细节上的变化。当我看到3.7的run()函数时,也发现一年前基于3.6的asnycio写的爬虫不那么优雅了。
这种变化,一方面改善了asyncio本身的性能和使用方便程度,但另一方面也增加了我们使用者的学习成本、Python升级带来的改造的成本。如果你以消极的态度抵制这种变化,可以去学习golang,C++来实现你的程序;如果你以积极的态度迎接这种变化,可以更快的掌握这种变化,并优雅 高效的实现你的程序。
只要你喜欢用Python写程序解决问题,那么就接受并掌握这种变化吧。其实,那种语言不在变,那种技术不在前进。作为程序员,你只有不断地学习和前进。
(3)uvloop
uvloop是用Cython写的,基于libuv这个C语言实现的高性能异步I/O库。asyncio自己的事件循环是用Python写的,用uvloop替换asyncio自己的事件循环可以是asyncio的速度更快。并且使用相当简洁:
总结
(1)异步IO用在费时的IO操作上以提高程序整体效率。
(2)同步和异步,阻塞和非阻塞就是方法和现象。
(3)Python的异步历史很复杂,然而目前给我们用的已经很优雅,记住以下三点:
(a) Python 3.7
(b) await,async
(c) IO的时候用
PS:写这种总结性的文章很累人,覆盖的知识面广,回溯的历史长,各种查资料和思考,耗费一整个周日的时间,难免有错误和遗漏,大家积极留言讨论挑毛病哦。下一篇继续asyncio的话题,结合3.7的版本探讨具体使用异步IO。
领取专属 10元无门槛券
私享最新 技术干货