这是Tornado系列的开篇,Tornado作为异步web框架2010年发布1.0版本。可以说Python社区搞了这么多年,为了获得比多线程多进程更高的性能,经历了Twisted、Tornado、yield、gevent、yield from、asyncio。也不知道什么时候才是一个尽头~~。Tornado过了这么多年还算开发活跃(一个大神扛起了一片天啊),不同于多线程模型的一潭死水,看它的代码你会发现它还是紧跟潮流的,而且有一点。它的内部虽然一直在变动,但是提供给用户的使用接口是很稳定的,后续就会发现这个坑有多大
截止目前最新版本的代码是4.5,排除测试总代码量大约为12000行。我个人喜欢看精简版本的代码,所以本文为1.0.0版本。总代码量为4200行,比较容易看懂。毕竟别人优化了八年,期待一个星期精读能搞明白各种工程优化是不现实的。
IOLoop
说到异步非阻塞大多数人都知道是异步IO模型(select、poll、epoll、kqueue)。没错在阻塞模型中比如使用socket.recv
它会主线程造成阻塞。这直接导致了在python中需要使用多线程或者多进程模型才能够同时处理多个请求。可是在异步模型中允许对文件描述符进行监听,操作系统提供功能,当文件的状态发生变化(可读、可写、异常)程序能够获得这一状态。对我们来说就是当明确知道某文件存在什么事件的时候去触发相应的处理函数。注意,这就是一个回调、回调、回调(重要的事情说三遍)
可以把IOLoop当做一个停不下来的死循环,类似下面这样
你只需要记住以下几点
- IOLoop是一个单例
- IOLoop提供了处理三种情况的方法,分别是回调形式(_callback)、定时器形式(_timeout)、网络IO(selectors)
- 最后是一个while 1的死循环
Callback
正因为IOLoop是一个单例。所以所有的函数最终都通过add_callback、add_timeout、add_handler放置到了IOLoop下面,最终被执行start依次调用,再调用的过程中,这些函数又使用ioloop.IOLoop.instance().add_callback等不断的加入一些事件处理。导致形成了一个callback链条,让我们最终得到结果
如上例.当我们调用get()开始执行代码的时候它只是开启了第一步创建了一个socket链接并在selector中注册了一个事件,等待READ事件被触发,k.data()被调用,即connected函数被调用,发送数据之后再次注册READ事件。依然等待直到k.data()被调用,即readable被调用。最后直至数据读取完成。表示整个过程结束
优化
观察IOLoop例子中的start死循环可以知道。它其实也是伴随着阻塞的,在self.ioloop.select(timeout=0.2)
中如果持续0.2秒没有等待的IO事件发生,那么会阻塞0.2秒,看似0.2秒也不长,但其实非常大量的事件并不是IO事件,而是在self._callback集合中。因此从效率方面考虑添加_callback的时候触发IO事件,让select不再去阻塞那0.2秒,因此创建了一个管道对象来解决了这个问题,当添加的_callback的时候同时向管道写入一个字符。那么就不存在0.2秒的阻塞了
应用一 PeriodicCallback
ioloop.py下面有一个非常简单的PeriodicCallback类,它的作用是定期将callback加入到IOLoop的_timeout列表中。比如你需要每隔三十分钟去清理一次临时文件夹产生的垃圾文件等等。简易实现大概是这样
再执行start之后添加到了_timeout。待时间到达后执行一次然后再调用start,如此反复