python-tornado 相对于其他的网络框架,最为人知的特点就是异步网络和非阻塞I/O,以及以用同步的方式写异步的代码等Tornado 。然鹅,在最自然(不做特别处理)的情况下,Tornado 其实是阻塞的。
先看一个阻塞的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import tornado.ioloopimport tornado.webimport timeclass IndexHandler (tornado.web.RequestHandler) : def get (self) : self.write("index" ) class Blocking (tornado.web.RequestHandler) : def get (self, *args, **kwargs) : time.sleep(10 ) self.write("blocking" ) if __name__ == '__main__' : tornado.web.Application(handlers=[ ('/' , IndexHandler), ('/blocking' , Blocking) ], autoreload=True ).listen(5000 , 'localhost' ) tornado.ioloop.IOLoop().current().start()
如果先访问 localhost:5000/blocking
,那么在接下来的10s内再访问 localhost:5000
就会发现一直没有相应,一直到等到 10s 结束,处理完 blocking
的请求。(虽然这并不是一个正确的例子,但是接下来记录的正是对协程
的进一步理解)
通过各方搜索,肯定能够找到下面的方法来解决整个Tornado阻塞在这个阻塞请求上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import tornado.ioloopimport tornado.webimport timefrom tornado import genclass IndexHandler (tornado.web.RequestHandler) : def get (self) : self.write("index" ) class Blocking (tornado.web.RequestHandler) : @gen.coroutine def get (self, *args, **kwargs) : yield gen.sleep(10 ) self.write("blocking" ) if __name__ == '__main__' : tornado.web.Application(handlers=[ ('/' , IndexHandler), ('/blocking' , Blocking) ], autoreload=True ).listen(5000 , 'localhost' ) tornado.ioloop.IOLoop().current().start()
此时再次先访问 localhost:5000/blocking
,会发现访问这个请求的时候依然需要等待10s才会有响应,然而不同的是在这期间访问 localhost:5000
变的很顺畅,就像网站上没有什么阻塞的事件在运行一样(实际上也并没有)。大多数人在这里就开始笃信tornado中异步的强大,以及用 协程
将阻塞变成非阻塞的万能(就像当年的我23333),其实这里只是用错了栗子。之所以说用错,是因为 gen.sleep
并不是将原本阻塞的方法变成了非阻塞,只是偷换了 time.sleep
的同步性,取而代之延迟执行的表象。
查看一下 tornado 中 gen.sleep
究竟做了什么,就会发现:
gen.sleep 源码片段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def sleep (duration) : """Return a `.Future` that resolves after the given number of seconds. When used with ``yield`` in a coroutine, this is a non-blocking analogue to `time.sleep` (which should not be used in coroutines because it is blocking):: yield gen.sleep(0.5) Note that calling this function on its own does nothing; you must wait on the `.Future` it returns (usually by yielding it). .. versionadded:: 4.1 """ f = Future() IOLoop.current().call_later(duration, lambda : f.set_result(None )) return f
这个 sleep
并没有真的sleep,只是在事件循环中添加了一个延迟的回调,如果遇到真的需要(连续不断)占用炒鸡长时间的任务运行,整个网站一定会阻塞。其实仔细想想也并不困难,单开一个Tornado进程,没有多线程,整个网站也只能在单线程环境下运行,如果某一个任务毫无间断地在整个网站的时间片上运行,当然会造成阻塞。于是乎,接下来能想到的处理方法就是多线程的方法了,tornado也支持这种方式实现多线程非阻塞。修改之后的代码可能类似这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import tornado.ioloopimport tornado.webimport timefrom tornado.concurrent import run_on_executorfrom concurrent.futures import ThreadPoolExecutorclass IndexHandler (tornado.web.RequestHandler) : def get (self) : self.write("index" ) class Blocking (tornado.web.RequestHandler) : executor = ThreadPoolExecutor(4 ) @run_on_executor def get (self, *args, **kwargs) : time.sleep(10 ) self.write("blocking" ) if __name__ == '__main__' : tornado.web.Application(handlers=[ ('/' , IndexHandler), ('/blocking' , Blocking) ], autoreload=True ).listen(5000 , 'localhost' ) tornado.ioloop.IOLoop().current().start()
完全不需要 tornado 协程的支持,只需要一个额外的线程池就好了,这样也能达到曾经用协程时候的效果,而且这次是真的非阻塞了,毕竟阻塞的任务跑在了其他线程上。这里用到了python2没有自带的 concurrent
库,python2下面需要额外安装。然而,C Python中的多线程涉及到的GIL依然会导致依赖计算资源的任务阻塞整个进程。于是还会有更多基于消息队列(比如 Celery、Redis)的方式,在分布式的环境中减少阻塞的发生。当然,我并不会继续沿着这条路走下去了,如此下去整个网站的确会表现优异,然而,各种程序依赖不仅会降低可控性,提高维护难度,也减少了思考的余地。
最近就走到了这步余地。刚刚修好一个问题,到了午饭时间,就看到老大眉头紧皱的来跟我说,”我发现了一个很大的bug”。话还没说完,我也是心里一紧,难道是把自己的漏洞越修越大了咩=。=,”我发现Tornado是阻塞的”。于是之后才花了时间一起探索了一番tornado的协程异步。就在准备用线程池来暂时解决问题的时候,忽然想起来同样用协程来解决问题的 gevent
,中的一个栗子:
1 2 3 4 5 6 7 8 9 10 from gevent import monkey; monkey.patch_socket()import geventimport timedef f (n) : for i in range(n): print i gevent.sleep(0 ) gevent.joinall([gevent.spawn(f, 5 ) for _ in range(5 )])
其中有关键的一句 gevent.sleep(0)
,表面上看毫无作用(睡了0 s),然而实际作用却是关键的,能够让任务从协程中切换出来。再想想一般将生成器当作协程来用的时候通过 yield
将协程切换出来,以及 Tornado 中的协程异步其实也是通过 yield
进行协程调度,那么再看看 time.sleep(10)
之所以在使用了传说中的非阻塞魔法之后依然发生阻塞,就会发现,真正的原因其实是 time.sleep(10)
过于庞大,并且没有协程插手进行调度的余地。把这个阻塞的方法换成这样:
1 2 3 4 5 6 class Blocking (tornado.web.RequestHandler) : @gen.coroutine def get (self, *args, **kwargs) : for _ in range(1000 ): yield time.sleep(0.01 ) self.write("blocking" )
就会发现虽然也是用的 time.sleep
这个阻塞方法,但是并没有阻塞现象(因为只阻塞了0.01s就去响应 /
路径去了)。当然,这是最简单的将阻塞变成非阻塞的情况,实际上肯定会在其他地方完成逻辑响应,将结果交给 tornado去响应给请求。比如这样:
1 2 3 4 5 6 7 8 9 10 class Blocking (tornado.web.RequestHandler) : @gen.coroutine def get (self, *args, **kwargs) : yield sleepy() self.write("blocking" ) @gen.coroutine def sleepy () : for _ in range(1000 ): yield time.sleep(0.01 )
然而对于消耗CPU资源(计算型)的任务而言,首先能不能找到这个协程切换的点很难说,然后这个计算型的协程会不会被切换更是另外一回事。协程并不是万能的,再怎么说也只有一个线程在运行,再加上 python 运行效率的低下,大多数人没有停留在协程上还是没有问题的。