前言
去年有一段时间一直在研究各种python协程框架,包括gevent, asyncio, tornado。阅读tornado的源码还是两个多月前的事了,一直想写一篇文章出来整理整理,但不知道从何处开始下笔。如果贴上一段段源码,然后通过语言来描述各种流程,这种类型的文章网上也有不少,况且这样子的讲解对于读者来说可能会比较乏味。
我希望我对于源码分析的博文能够通过贴上更容易理解的图(当然也会有一些代码来辅助讲解),这样的分享对读者来说会更加容易读懂,也更有价值。对自己要求高了,反而更难下笔,在试图画图的过程中,发现其实有好多细节自己也没有琢磨透,导致在如何组织这幅流程图的问题上斟酌了好久,不过好在最后终于捯饬出了一张自己觉得还算及格的流程图,作完图的时候我感觉比起之前刚阅读完代码时候的理解又上了一个层次。
流程图
tornado执行协程的方式有很多,但协程内部的运行原理没什么区别,这篇文章以IOLoop
中的run_sync
函数作为入口进行介绍。在开始进行分析之前,先把流程图贴上,其中的细节后面会通过代码辅助的方式一一讲解。
在理解tornado运行原理的过程中,我是通过写一个demo,然后在源码中到处打断点,然后调试的方式,一遍遍走,到最后慢慢地理解。顺便也把我的demo代码贴上吧(看过我之前的一篇译文的读者可能会发现,这个demo是从那儿仿照过来的)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import random import time from tornado import gen from tornado.ioloop import IOLoop @gen.coroutine def get_url(url): wait_time = random.randint(1, 4) yield gen.sleep(wait_time) print('URL {} took {}s to get!'.format(url, wait_time)) raise gen.Return((url, wait_time)) @gen.coroutine def outer_coroutine(): before = time.time() coroutines = [get_url(url) for url in ['URL1', 'URL2', 'URL3']] result = yield coroutines after = time.time() print(result) print('total time: {} seconds'.format(after - before)) if __name__ == '__main__': IOLoop.current().run_sync(outer_coroutine) |
有兴趣的读者可以自己去执行一下玩玩,输出类似于这样:
1 2 3 4 5 |
URL URL1 took 1s to get! URL URL2 took 2s to get! URL URL3 took 2s to get! [('URL1', 1), ('URL2', 2), ('URL3', 2)] total time: 2.00353884697 seconds |
Coroutine
起初我以为调用协程后,返回的是一个生成器对象,毕竟gen.coroutine
装饰在一个函数或者生成器上。看了源码发现,其实每次调用一个协程,它在获取了生成器对象之后,同时又对它执行了next
操作来获取生成器内部yield出来的值,这个可以是一个值,当然也可以是一个由内部协程嵌套调用返回的future对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# gen.py def _make_coroutine_wrapper(func, replace_callback): @functools.wraps(func) def wrapper(*args, **kwargs): future = TracebackFuture() # 省略n行 try: result = func(*args, **kwargs) # 省略n个except else: if isinstance(result, types.GeneratorType): try: orig_stack_contexts = stack_context._state.contexts yielded = next(result) # 如果func内部有yield关键字,result是一个生成器 # 如果func内部又调用了其它协程,yielded将会是由嵌套协程返回的future对象 # 省略n行 # 省略n个except else: Runner(result, future, yielded) try: return future finally: future = None future.set_result(result) return future return wrapper |
Future
我觉得Future
在tornado中是一个很奇妙的对象,它是一个穿梭于协程和调度器之间的信使。提供了回调函数注册(当异步事件完成后,调用注册的回调)、中间结果保存、嵌套协程唤醒父协程(通过Runner实现)等功能。Coroutine和Future是一一对应的,可以从上节gen.coroutine装饰器的实现中看到。每调用一个协程,表达式所返回的就是一个Future对象,它所表达的意义为:这个协程的内部各种异步逻辑执行完毕后,会把结果保存在这个Future中,同时调用这个Future中指定的回调函数,而future中的回调函数是什么时候被注册的呢?那就是当前——你通过调用协程,返回了这个future对象的时候:
我们看看demo代码中run_܀始下笔。如果贴上一段段源码,然后通过语言来描述各种流程,这种类型的文章网上也有不少,况且这样子的讲解对于读者来说可能会比较乏味。
我希望我对于源码分析的博文能够通过贴上更容易理解的图(当然也会有一些代码来辅助讲解),这样的分享对读者来说会更加容易读懂,也更有价值。对自己要求高了,反而更难下笔,在试图画图的过程中,发现其实有好多细节自己也没有琢磨透,导致在如何组织这幅流程图的问题上斟酌了好久,不过好在最后终于捯饬出了一张自己觉得还算及格的流程图,作完图的时候我感觉比起之前刚阅读完代码时候的理解又上了一个层次。
流程图
tornado执行协程的方式有很多,但协程内部的运行原理没什么区别,这篇文章以IOLoop
中的run_sync
函数作为入口进行介绍。在开始进行分析之前,先把流程图贴上,其中的细节后面会通过代码辅助的方式一一讲解。
在理解tornado运行原理的过程中,我是通过写一个demo,然后在源码中到处打断点,然后调试的方式,一遍遍走,到最后慢慢地理解。顺便也把我的demo代码贴上吧(看过我之前的一篇译文的读者可能会发现,这个demo是从那儿仿照过来的)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import random import time from tornado import gen from tornado.ioloop import IOLoop @gen.coroutine def get_url(url): wait_time = random.randint(1, 4) yield gen.sleep(wait_time) print('URL {} took {}s to get!'.format(url, wait_time)) raise gen.Return((url, wait_time)) @gen.coroutine def outer_coroutine(): before = time.time() coroutines = [get_url(url) for url in ['URL1', 'URL2', 'URL3']] result = yield coroutines after = time.time() print(result) print('total time: {} seconds'.format(after - before)) if __name__ == '__main__': IOLoop.current().run_sync(outer_coroutine) |
有兴趣的读者可以自己去执行一下玩玩,输出类似于这样:
1 2 3 4 5 |
URL URL1 took 1s to get! URL URL2 took 2s to get! URL URL3 took 2s to get! [('URL1', 1), ('URL2', 2), ('URL3', 2)] total time: 2.00353884697 seconds |
Coroutine
起初我以为调用协程后,返回的是一个生成器对象,毕竟gen.coroutine
装饰在一个函数或者生成器上。看了源码发现,其实每次调用一个协程,它在获取了生成器对象之后,同时又对它执行了next
操作来获取生成器内部yield出来的值,这个可以是一个值,当然也可以是一个由内部协程嵌套调用返回的future对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# gen.py def _make_coroutine_wrapper(func, replace_callback): @functools.wraps(func) def wrapper(*args, **kwargs): future = TracebackFuture() # 省略n行 try: result = func(*args, **kwargs) # 省略n个except else: if isinstance(result, types.GeneratorType): try: orig_stack_contexts = stack_context._state.contexts yielded = next(result) # 如果func内部有yield关键字,result是一个生成器 # 如果func内部又调用了其它协程,yielded将会是由嵌套协程返回的future对象 # 省略n行 # 省略n个except else: Runner(result, future, yielded) try: return future finally: future = None future.set_result(result) return future return wrapper |
Future
我觉得Future
在tornado中是一个很奇妙的对象,它是一个穿梭于协程和调度器之间的信使。提供了回调函数注册(当异步事件完成后,调用注册的回调)、中间结果保存、嵌套协程唤醒父协程(通过Runner实现)等功能。Coroutine和Future是一一对应的,可以从上节gen.coroutine装饰器的实现中看到。每调用一个协程,表达式所返回的就是一个Future对象,它所表达的意义为: