tornado-ioloop的理解
来源:互联网 发布:java微信接口开发demo 编辑:程序博客网 时间:2024/05/16 05:29
tornado 优秀的大并发处理能力得益于它的 web server 从底层开始就自己实现了一整套基于 epoll 的单线程异步架构(其他 python web 框架的自带 server 基本是基于 wsgi 写的简单服务器,并没有自己实现底层结构。 关于 wsgi 详见之前的文章: 自己写一个 wsgi 服务器运行 Django 、Tornado 应用)。 那么 tornado.ioloop
就是 tornado web server 最底层的实现。
看 ioloop 之前,我们需要了解一些预备知识,有助于我们理解 ioloop。
epoll
ioloop 的实现基于 epoll ,那么什么是 epoll? epoll 是Linux内核为处理大批量文件描述符而作了改进的 poll 。
那么什么又是 poll ? 首先,我们回顾一下, socket 通信时的服务端,当它接受( accept )一个连接并建立通信后( connection )就进行通信,而此时我们并不知道连接的客户端有没有信息发完。 这时候我们有两种选择:
- 一直在这里等着直到收发数据结束;
- 每隔一定时间来看看这里有没有数据;
第二种办法要比第一种好一些,多个连接可以统一在一定时间内轮流看一遍里面有没有数据要读写,看上去我们可以处理多个连接了,这个方式就是 poll / select 的解决方案。 看起来似乎解决了问题,但实际上,随着连接越来越多,轮询所花费的时间将越来越长,而服务器连接的 socket 大多不是活跃的,所以轮询所花费的大部分时间将是无用的。为了解决这个问题, epoll 被创造出来,它的概念和 poll 类似,不过每次轮询时,他只会把有数据活跃的 socket 挑出来轮询,这样在有大量连接时轮询就节省了大量时间。
对于 epoll 的操作,其实也很简单,只要 4 个 API 就可以完全操作它。
epoll_create
用来创建一个 epoll 描述符( 就是创建了一个 epoll )
epoll_ctl
操作 epoll 中的 event;可用参数有:
而事件的监听方式有七种,而我们只需要关心其中的三种:
epoll_wait
就是让 epoll 开始工作,里面有个参数 timeout,当设置为非 0 正整数时,会监听(阻塞) timeout 秒;设置为 0 时立即返回,设置为 -1 时一直监听。
在监听时有数据活跃的连接时其返回活跃的文件句柄列表(此处为 socket 文件句柄)。
close
关闭 epoll
现在了解了 epoll 后,我们就可以来看 ioloop 了 (如果对 epoll 还有疑问可以看这两篇资料: epoll 的原理是什么、百度百科:epoll)
tornado.ioloop
很多初学者一定好奇 tornado 运行服务器最后那一句 tornado.ioloop.IOLoop.current().start()
到底是干什么的。 我们先不解释作用,来看看这一句代码背后到底都在干什么。
先贴 ioloop 代码:
IOLoop 类首先声明了 epoll 监听事件的宏定义,当然,如前文所说,我们只要关心其中的 EPOLLIN 、 EPOLLOUT 、 EPOLLERR 就行。
类中的方法有很多,看起来有点晕,但其实我们只要关心 IOLoop 核心功能的方法即可,其他的方法在明白核心功能后也就不难理解了。所以接下来我们着重分析核心代码。
instance
、 initialized
、 install
、 clear_instance
、 current
、 make_current
、 clear_current
这些方法不用在意细节,总之现在记住它们都是为了让 IOLoop 类变成一个单例,保证从全局上调用的都是同一个 IOLoop 就好。
你一定疑惑 IOLoop 为何没有 __init__
, 其实是因为要初始化成为单例,IOLoop 的 new 函数已经被改写了,同时指定了 initialize
做为它的初始化方法,所以此处没有 __init__
。 说到这,ioloop 的代码里好像没有看到 new
方法,这又是什么情况? 我们先暂时记住这里。
接着我们来看这个初始化方法:
what? 里面只是判断了是否第一次初始化或者调用 self.make_current()
初始化,而 make_current()
里也仅仅是把实例指定为自己,那么初始化到底去哪了?
然后再看看 start()
、 run()
、 close()
这些关键的方法都成了返回 NotImplementedError
错误,全部未定义?!跟网上搜到的源码分析完全不一样啊。 这时候看下 IOLoop 的继承关系,原来问题出在这里,之前的 tornado.ioloop 继承自 object 所以所有的一切都自己实现,而现在版本的 tornado.ioloop 则继承自 Configurable
看起来现在的 IOLoop 已经成为了一个基类,只定义了接口。 所以接着看 Configurable
代码:
tornado.util.Configurable
之前我们寻找的 __new__
出现了! 注意其中这句: impl = cls.configured_class()
impl 在这里就是 epoll ,它的生成函数是 configured_class()
, 而其方法里又有 base.__impl_class = cls.configurable_default()
,调用了 configurable_default()
。而 Configurable
的 configurable_default()
:
显然也是个接口,那么我们再回头看 ioloop 的 configurable_default()
:
原来这是个工厂函数,根据不同的操作系统返回不同的事件池(linux 就是 epoll, mac 返回 kqueue,其他就返回普通的 select。 kqueue 基本等同于 epoll, 只是不同系统对其的不同实现)
现在线索转移到了 tornado.platform.epoll.EPollIOLoop
上,我们再来看看 EPollIOLoop
:
tornado.platform.epoll.EPollIOLoop
EPollIOLoop
完全继承自 PollIOLoop
(注意这里是 PollIOLoop 不是 IOLoop)并只是在初始化时指定了 impl 是 epoll,所以看起来我们用 IOLoop 初始化最后初始化的其实就是这个 PollIOLoop,所以接下来,我们真正需要理解和阅读的内容应该都在这里:
tornado.ioloop.PollIOLoop
果然, PollIOLoop 继承自 IOLoop 并实现了它的所有接口,现在我们终于可以进入真正的正题了
ioloop 分析
首先要看的是关于 epoll 操作的方法,还记得前文说过的 epoll 只需要四个 api 就能完全操作嘛? 我们来看 PollIOLoop 的实现:
epoll 操作
epoll_ctl:这个三个方法分别对应 epoll_ctl 中的 add 、 modify 、 del 参数。 所以这三个方法实现了 epoll 的 epoll_ctl 。
epoll_create:然后 epoll 的生成在前文 EPollIOLoop 的初始化中就已经完成了:super(EPollIOLoop, self).initialize(impl=select.epoll(), **kwargs)
。 这个相当于 epoll_create 。
epoll_wait:epoll_wait 操作则在 start()
中:event_pairs = self._impl.poll(poll_timeout)
epoll_close:而 epoll 的 close 则在 PollIOLoop 中的 close
方法内调用: self._impl.close()
完成。
initialize
接下来看 PollIOLoop 的初始化方法中作了什么:
除了注释中的解释,还有几点补充:
- close_exec 的作用: 子进程在fork出来的时候,使用了写时复制(COW,Copy-On-Write)方式获得父进程的数据空间、 堆和栈副本,这其中也包括文件描述符。刚刚fork成功时,父子进程中相同的文件描述符指向系统文件表中的同一项,接着,一般我们会调用exec执行另一个程序,此时会用全新的程序替换子进程的正文,数据,堆和栈等。此时保存文件描述符的变量当然也不存在了,我们就无法关闭无用的文件描述符了。所以通常我们会fork子进程后在子进程中直接执行close关掉无用的文件描述符,然后再执行exec。 所以 close_exec 执行的其实就是 关闭 + 执行的作用。 详情可以查看: 关于linux进程间的close-on-exec机制
- Waker(): Waker 封装了对于管道 pipe 的操作:123456789101112131415161718192021222324252627282930313233343536373839404142defset_close_exec(fd):flags=fcntl.fcntl(fd,fcntl.F_GETFD)fcntl.fcntl(fd,fcntl.F_SETFD,flags|fcntl.FD_CLOEXEC)def_set_nonblocking(fd):flags=fcntl.fcntl(fd,fcntl.F_GETFL)fcntl.fcntl(fd,fcntl.F_SETFL,flags|os.O_NONBLOCK)classWaker(interface.Waker):def__init__(self):r,w=os.pipe()_set_nonblocking(r)_set_nonblocking(w)set_close_exec(r)set_close_exec(w)self.reader=os.fdopen(r,"rb",0)self.writer=os.fdopen(w,"wb",0)deffileno(self):returnself.reader.fileno()defwrite_fileno(self):returnself.writer.fileno()defwake(self):try:self.writer.write(b"x")exceptIOError:passdefconsume(self):try:whileTrue:result=self.reader.read()ifnotresult:breakexceptIOError:passdefclose(self):self.reader.close()self.writer.close()
可以看到 waker 把 pipe 分为读、 写两个管道并都设置了非阻塞和close_exec
。 注意wake(self)
方法中:self.writer.write(b"x")
直接向管道中写入随意字符从而释放管道。start
ioloop 最核心的部分:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697def start(self):if self._running: # 判断是否已经运行raise RuntimeError("IOLoop is already running")self._setup_logging()if self._stopped:self._stopped = False # 设置停止为假returnold_current = getattr(IOLoop._current, "instance", None)IOLoop._current.instance = selfself._thread_ident = thread.get_ident() # 获得当前线程标识符self._running = True # 设置运行old_wakeup_fd = Noneif hasattr(signal, 'set_wakeup_fd') and os.name == 'posix':try:old_wakeup_fd = signal.set_wakeup_fd(self._waker.write_fileno())if old_wakeup_fd != -1:signal.set_wakeup_fd(old_wakeup_fd)old_wakeup_fd = Noneexcept ValueError:old_wakeup_fd = Nonetry:while True: # 服务器进程正式开始,类似于其他服务器的 serve_foreverwith self._callback_lock: # 加锁,_callbacks 做为临界区不加锁进行读写会产生脏数据callbacks = self._callbacks # 读取 _callbacksself._callbacks = []. # 清空 _callbacksdue_timeouts = [] # 用于存放这个周期内已过期( 已超时 )的任务if self._timeouts: # 判断 _timeouts 里是否有数据now = self.time() # 获取当前时间,用来判断 _timeouts 里的任务有没有超时while self._timeouts: # _timeouts 有数据时一直循环, _timeouts 是个最小堆,第一个数据永远是最小的, 这里第一个数据永远是最接近超时或已超时的if self._timeouts[0].callback is None: # 超时任务无回调heapq.heappop(self._timeouts) # 直接弹出self._cancellations -= 1 # 超时计数器 -1elif self._timeouts[0].deadline 512and self._cancellations > (len(self._timeouts) >> 1)): # 当超时计数器大于 512 并且 大于 _timeouts 长度一半( >> 为右移运算, 相当于十进制数据被除 2 )时,清零计数器,并剔除 _timeouts 中无 callbacks 的任务self._cancellations = 0self._timeouts = [x for x in self._timeoutsif x.callback is not None]heapq.heapify(self._timeouts) # 进行 _timeouts 最小堆化for callback in callbacks:self._run_callback(callback) # 运行 callbacks 里所有的 calllbackfor timeout in due_timeouts:if timeout.callback is not None:self._run_callback(timeout.callback) # 运行所有已过期任务的 callbackcallbacks = callback = due_timeouts = timeout = None # 释放内存if self._callbacks: # _callbacks 里有数据时poll_timeout = 0.0 # 设置 epoll_wait 时间为0( 立即返回 )elif self._timeouts: # _timeouts 里有数据时poll_timeout = self._timeouts[0].deadline - self.time()# 取最小过期时间当 epoll_wait 等待时间,这样当第一个任务过期时立即返回poll_timeout = max(0, min(poll_timeout, _POLL_TIMEOUT))# 如果最小过期时间大于默认等待时间 _POLL_TIMEOUT = 3600,则用 3600,如果最小过期时间小于0 就设置为0 立即返回。else:poll_timeout = _POLL_TIMEOUT # 默认 3600 s 等待时间if not self._running: # 检查是否有系统信号中断运行,有则中断,无则继续breakif self._blocking_signal_threshold is not None:signal.setitimer(signal.ITIMER_REAL, 0, 0) # 开始 epoll_wait 之前确保 signal alarm 都被清空( 这样在 epoll_wait 过程中不会被 signal alarm 打断 )try:event_pairs = self._impl.poll(poll_timeout) # 获取返回的活跃事件队except Exception as e:if errno_from_exception(e) == errno.EINTR:continueelse:raiseif self._blocking_signal_threshold is not None:signal.setitimer(signal.ITIMER_REAL,self._blocking_signal_threshold, 0) # epoll_wait 结束, 再设置 signal alarmself._events.update(event_pairs) # 将活跃事件加入 _eventswhile self._events:fd, events = self._events.popitem() # 循环弹出事件try:fd_obj, handler_func = self._handlers[fd] # 处理事件handler_func(fd_obj, events)except (OSError, IOError) as e:if errno_from_exception(e) == errno.EPIPE:passelse:self.handle_callback_exception(self._handlers.get(fd))except Exception:self.handle_callback_exception(self._handlers.get(fd))fd_obj = handler_func = Nonefinally:self._stopped = False # 确保发生异常也继续运行if self._blocking_signal_threshold is not None:signal.setitimer(signal.ITIMER_REAL, 0, 0) # 清空 signal alarmIOLoop._current.instance = old_currentif old_wakeup_fd is not None:signal.set_wakeup_fd(old_wakeup_fd) # 和 start 开头部分对应,但是不是很清楚作用,求老司机带带路stop
1234defstop(self):self._running=Falseself._stopped=Trueself._waker.wake()这个很简单,设置判断条件,然后调用
self._waker.wake()
向 pipe 写入随意字符唤醒 ioloop 事件循环。 over!总结
噗,写了这么长,终于写完了。 经过分析,我们可以看到, ioloop 实际上是对 epoll 的封装,并加入了一些对上层事件的处理和 server 相关的底层处理。
- tornado-ioloop的理解
- [转]tornado ioloop start 的过程
- 深入tornado中的ioLoop
- 深入tornado中的ioLoop
- tornado IOLoop源码阅读
- 14.Tornado高性能的秘密:ioloop对象分析 (副标题:IOLoop是个事件循环)
- Tornado高性能的秘密:ioloop对象分析 (副标题:IOLoop是个事件循环)
- Tornado 之 IOLoop类分析
- tornado源码阅读--ioloop篇
- Tornado源码分析之IOLoop
- tornado学习笔记(一):如何给ioloop.run_sync()中调用的函数传入参数
- tornado之ioloop类源码分析
- tornado长轮询的理解
- Django和Tornado 的理解
- ImportError: No module named tornado.ioloop 记录过程
- 基于IOLoop的Web Server
- tornado源码分析系列 [网络层 IOLoop类] 优秀系列文章可以看看
- Tornado异步框架理解
- String To Map
- 探寻C++读取文件最快的方式
- linux基础篇-RHEL实现服务或脚本开机自启动的方法
- 学习笔记-JavaScript系列之DOM节点知识总结
- linux下 lsof命令详解
- tornado-ioloop的理解
- 线段树离散化
- 原码 反码 补码 移码
- spaly区间翻转,分裂,合并!UVA11922,UVA11996也是个不错的spaly
- MySQL——1
- Android常用控件之按钮选择器/九妹图,按钮点击事件+页面跳转+Toast,单选框.时间日期控件 /时间日期对话框
- Spring结合Shiro进行权限控制
- HDU2046
- Hadoop