转自:http://www.binss.me/blog/analyse-the-implement-of-coroutine-in-tornado/

什么是协程

以下是Wiki的定义:

Coroutines are computer program components that generalize subroutines for nonpreemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations. Coroutines are well-suited for implementing more familiar program components such as cooperative tasks, exceptions, event loop, iterators, infinite lists and pipes.

我们经常使用的函数又称子例程(subroutine),往往只有一个入口(函数的第一行),一个出口(return、抛出异常)。子例程在入口时获得控制权,在出口时把控制权交还给调用者。一旦交还控制权,就意味着例程的结束,函数中做的所有工作以及保存在局部变量中的数据都将被释放。而协程可以有多个入口点,允许从一个入口点执行到下一个入口点之前暂停,保存执行状态;等到合适的时机恢复执行状态,从下一个入口点重新开始执行。

"Subroutines are special cases of ... coroutines." –Donald Knuth.

可以把协程看做是子例程的泛化形式。

在Python中,协程基于生成器。它可以有多个出口和入口。出口有两种类型:一种是return,用于永久交还控制权,效果同子例程;另一种是yield,用于暂时交还控制权,函数将来还会收回控制权。当然入口也有两种:一种是函数的第一行;另一种是上次yield的那一行。

本文在《Python中的迭代器和生成器》一文的基础上,通过对Tornado框架中的协程的分析,进行协程的学习。

Tornado中的协程

Tornado典型的协程例子:

  class GenAsyncHandler(RequestHandler):
  @gen.coroutine
  def get(self):
  http_client = AsyncHTTPClient()
  response = yield http_client.fetch("http://example.com")
  do_something_with_response(response)
  self.render("template.html")

我们为这个Handler的get()添加了装饰器gen.coroutine,从而使其成为一个协程。不难理解,面对耗时的操作,利用协程可以达到异步的效果,需要等待的时候yield出去,等待结束后重新回来继续执行。如例子中get需要等待http_client.fetch("http://example.com")的结果,因此通过yield返回;当获取到结果后,get函数从上次离开处继续运行,且response被赋值为http_client.fetch("http://example.com")的结果。

这主要涉及到以下几样东西:

  1. Future

    Future的设计目标是作为协程(coroutine)和IOLoop的媒介,从而将协程和IOLoop关联起来。

    Future在concurrent.py中定义,是异步操作结果的占位符,用于等待结果返回。通常作为函数IOLoop.add_future()的参数或gen.coroutine协程中yield的返回值。

    等到结果返回时,外部可以通过调用set_result()设置真正的结果,然后调用所有回调函数,恢复协程的执行。

  2. IOLoop

    IOLoop是一个I/O事件循环,用于调度socket相关的连接、响应、异步读写等网络事件,并支持在事件循环中添加回调(callback)和定时回调(timeout)。在支持的平台上,默认使用epoll进行I/O多路复用处理。

    IOLoop是Tornado的核心,绝大部分模块都依赖于IOLoop的调度。在协程运行环境中,IOLoop担任着协程调度器的角色,能够让暂停的协程重新获得控制权,从而能继续执行。

    IOLoop通过add_future()对Future的支持:

      def add_future(self, future, callback):
      assert is_future(future)
      callback = stack_context.wrap(callback)
      future.add_done_callback(lambda future: self.add_callback(callback, future))

    通过调用future的add_done_callback(),使当future在操作完成时,能够通过add_callback将callback添加到IOLoop中,让callback在IOLoop下一次迭代中执行(不在本轮是为了避免饿死)。

  3. Runner

    Runner和coroutine都在gen.py中定义。Runner由coroutine装饰器创建,用于维护挂起的回调函数及结果的相关信息,包括中间结果(future)和最终结果(result_future)。

  4. coroutine

    gen.coroutine是一个装饰器,负责将普通函数包装成协程。功能包括:

    • 调用函数,如果该函数有yield,则返回生成器。否则立即得到结果,不属于协程。

    • 通过next()执行生成器,如果未能结束(遇到yield),则生成中间类Runner对象,用于保存生成器、yield返回值和最终返回值。

    • 创建Runner。

结合以上几样东西,协程的具体流程整理如下:

每一次协程调用yield释放控制权后:

  -> [Runner]handle_yield
  处理yield返回的结果
   
  -> [Runner]ioloop.add_future(self.future, lambda f: self.run())
  将结果构造成future后添加到ioloop
   
  -> [future]add_done_callback(lambda future: self.add_callback(callback, future))
  将Runner.run()加入到完成时的回调函数列表中

???触发:

  -> [future]set_result
  已经得到future的结果,设置之
   
  -> [future]_set_done
  调用future所有回调函数(_callbacks)
   
  -> [ioloop]add_callback(callback, future)
  callback为[Runner]add_future添加的那个,即[Runner]self.run(),将在下一轮循环被执行
   
  -> [Runner]self.run()
  取出Runner的self.future(上次yield的返回值):
  1. 如果future未完成,return,流程结束,等待下一次set_result
   
  2. 如果future完成
  -> [Runner]yielded = self.gen.send(value)
  通过send把future的result发送给协程,并让其恢复执行:
   
  1. 如果协程结束(没yield了)
  -> [Runner]self.result_future.set_result
  设置最终的结果result_future
   
  2. 未结束(再次遇到yield)
  -> [Runner]handle_yield
  则再次调用handle_yield

从以上的流程可以看出,每一次协程调用yield释放控制权后的恢复,都依赖于set_result()的调用。当我们把目光看向总调度IOLoop时,可以发现IOLoop只是忠实地在每一轮迭代中调用那些就绪的回调函数,并没有主动调用set_result()的能力。

那么,在Tornado中,这个set_result()到底是谁调用?在何时调用?

搜遍了Tornado的代码,主要找到以下几个调用点:

  1. 在coroutine装饰器中,如果装饰的函数调用后直接结束(没yield),直接set_result()。

  2. 在[Runner]的run()中,如果调用send后协程结束,对result_future进行set_result()。

  3. 取决于yield后阻塞操作的具体实现,下面以例子中AsyncHTTPClient的fetch()来进行分析。

AsyncHTTPClient的fetch()是一个异步操作,其构造了一个HTTP请求,然后调用fetch_impl(),返回一个future。fetch_impl()取决于AsyncHTTPClient的具体实现,默认情况下,AsyncHTTPClient生成的是子类SimpleAsyncHTTPClient的实例,所以主要看SimpleAsyncHTTPClient的fetch_impl():

  def fetch_impl(self, request, callback):
  key = object()
  self.queue.append((key, request, callback))
  if not len(self.active) < self.max_clients:
  timeout_handle = self.io_loop.add_timeout(
  self.io_loop.time() + min(request.connect_timeout,
  request.request_timeout),
  functools.partial(self._on_timeout, key))
  else:
  timeout_handle = None
  self.waiting[key] = (request, callback, timeout_handle)
  self._process_queue()
  if self.queue:
  gen_log.debug("max_clients limit reached, request queued. "
  "%d active, %d queued requests." % (len(self.active), len(self.queue)))

fetch_impl()接受两个参数,request为fetch()中构造的HTTP请求,callback为fetch中的回调函数handle_response:

  def handle_response(response):
  if raise_error and response.error:
  future.set_exception(response.error)
  else:
  future.set_result(response)

在handle_response()中,调用了我们期待的set_result()。所以我们把目光转移到fetch_impl()的callback。在fetch_impl()中,函数先将callback加到队列中,然后通过_process_queue()处理掉,处理时调用_handle_request():

  def _handle_request(self, request, release_callback, final_callback):
  self._connection_class()(
  self.io_loop, self, request, release_callback,
  final_callback, self.max_buffer_size, self.tcp_client,
  self.max_header_size, self.max_body_size)

这里构造了一个_connection_class对象,即HTTPConnection。HTTPConnection通过self.tcp_client.connect()来建立TCP连接,然后通过该连接发送HTTP请求, 在超时(timeout)或完成(finish)时调用callback。tcp_client在建立异步TCP连接时,先进行DNS解析(又是协程),然后建立socket来构造IOStream对象,最后调用IOStream.connect()。在IOStream.connect()的过程中,我们看到了关键操作:

  self.io_loop.add_handler(self.fileno(), self._handle_events, self._state)

还记得我们前面说过的IOLoop吗?IOLoop可以添加socket、callback和timeout,并当它们就绪时调用相应的回调函数。这里add_handler处理的就是socket的多路复用,默认的实现是epoll。当epoll中该socket就绪时,相关函数得以回调。于是tcp_client读取socket内容获得HTTP response,handle_response()被调用,最终set_result()被调用。

到这里我们恍然大悟,AsyncHTTPClient的set_result()调用依赖于IO多路复用方案,这里是epoll,在epoll中相应socket的就绪的是set_result()得到调用的根本原因。而这个就绪事件的传递,离不开Tornado内建的IOStream,异步TCPClient、异步HTTPConnection,这些类的存在为我们隐藏了简单调用后的复杂性。因此当我们在用yield返回耗时操作时,如果不是Tornado的内建组件,则必须自己负责设计set_result的方案,比如以下代码:

  @gen.coroutine
  def add(self, a, b):
  future = Future()
  def callback(a, b):
  print("calculating the sum of %d + %d:" % (a,b))
  future.set_result(a+b)
  tornado.ioloop.IOLoop.instance().add_callback(callback, a, b)
   
  result = yield future
  print("%d + %d = %d" % (a, b, result))

通过手动将包含set_result()的回调函数加到IOLoop中,于是回调下一次迭代中执行,set_result()被调用,协程恢复控制权。

总结

实现高性能服务端,同步多进程、多线程风靡一时,然而由于其需要在内核态进行上下文切换,同步时还要加锁,导致并发性能低下。于是异步冒出来了,如Session、状态机,当然还有本文讨论的协程。

学习协程,主要的原因是在看SS代码中深受状态机代码的折磨(为啥要看SS的代码?你懂的)。因为在状态机中,逻辑代码被分割成多块分散在N个回调里(各种on_xxxxx),割裂了人的顺序性思维,在阅读代码、理解逻辑时跳来跳去令人痛不欲生。反观协程,真正做到了同步编码异步执行。

然而深挖协程的实现发现,协程的高性能和易编写易阅读是以后端框架复杂的封装为代价的。即使是在Python中我们拥有能够维护上下文的生成器,为了实现协程的调度,Tornado依然耗费了不少功夫:从定义Future对象用于等待结果返回,到使用coroutine装饰器将生成器的yield返回值封装成Runner,最后到set_result让Runner重新跑起来,而这些都依赖于IOLoop的调度。在经过层层跳转后达成了协程的调度目的,不得不感慨Tornado设计的巧妙。

不管怎么说,对于我等码农来说,协程代码写起来真的爽,读起来也也很爽,可以少死很多脑细胞,这就够了。

参考

http://www.tornadoweb.org/en/stable/guide/coroutines.html

http://blog.csdn.net/wyx819/article/details/45420017

分析Tornado的协程实现的更多相关文章

  1. flask之分析线程和协程

    flask之分析线程和协程 01 思考:每个请求之间的关系 我们每一个请求进来的时候都开一个进程肯定不合理,那么如果每一个请求进来都是串行的,那么根本实现不了并发,所以我们假定每一个请求进来使用的是线 ...

  2. Flask 之分析线程和协程

    目录 flask之分析线程和协程 01 思考:每个请求之间的关系 02 threading.local 03 通过字典自定义threading.local 04 通过setattr和getattr实现 ...

  3. Python开发【模块】:tornado.queues协程的队列

    协程的队列 协调生产者消费者协程. from tornado import gen from tornado.ioloop import IOLoop from tornado.queues impo ...

  4. PHP yield 分析,以及协程的实现,超详细版(上)

    参考资料 http://www.laruence.com/2015/05/28/3038.html http://php.net/manual/zh/class.generator.php http: ...

  5. 深入tornado中的协程

    tornado使用了单进程(当然也可以多进程) + 协程 + I/O多路复用的机制,解决了C10K中因为过多的线程(进程)的上下文切换 而导致的cpu资源的浪费. tornado中的I/O多路复用前面 ...

  6. tornado用户指引(二)------------tornado协程实现原理和使用(一)

    摘要:Tornado建议使用协程来实现异步调用.协程使用python的yield关键字来继续或者暂停执行,而不用编写大量的callback函数来实现.(在linux基于epoll的异步调用中,我们需要 ...

  7. tornado 协程的实现原理个人理解;

    tornado实现协程的原理主要是利用了(1)python里面的generator (2)future类和ioloop相互配合,两者之间的相互配合是通过gen.coroutine装饰器来实现的: 具体 ...

  8. qemu核心机制分析-协程coroutine

    关于协程coroutine前面的文章已经介绍过了,本文总结对qemu中coroutine机制的分析,qemu 协程coroutine基于:setcontext函数族以及函数间跳转函数siglongjm ...

  9. Tornado 协程

    同步异步I/O客户端 from tornado.httpclient import HTTPClient,AsyncHTTPClient def ssync_visit(): http_client ...

随机推荐

  1. iOS开发UI篇—iOS开发中三种简单的动画设置

    iOS开发UI篇—iOS开发中三种简单的动画设置 [在ios开发中,动画是廉价的] 一.首尾式动画 代码示例: // beginAnimations表示此后的代码要“参与到”动画中 [UIView b ...

  2. java基础之 内部类

    Java中的内部类共分为四种: 静态内部类static inner class (also called nested class) 成员内部类member inner class 局部内部类loca ...

  3. 新手入门之GDB调试

    写这篇文章算是对最近两天工作的一个经验总结吧. 要让可执行文件比较方便地在DGB上调试,在用gcc编译的时候要使用-g选项. 如何使用GDB启动被调试程序? "gdb path_to_deb ...

  4. Python的平凡之路(3)

     一.函数基本语法及特性 面向对象:(华山派)—类 —class 面向过程:(少林派)—过程 —df 函数式编程:逍遥派    —函数— df 一般的,在一个变化过程中,如果有两个变量x和y,并且对于 ...

  5. 应用容器Application container

    应用容器是最基本的组件,用于布局的容器. 属性 样式 事件 默认白边各24像素,默认为浏览器大小可以设置整体背景 边距等. 根应用文件就是第一个加载的文件.

  6. 软件项目第一个Sprint评论

    团队软件评论: 极速蜗牛:个人认为,内部测试版应该是实现内容而不是UI界面,难道要让那些懂电脑的人们都去玩用户界面吗?UI界面完全可以放到beta版再进行修改,美工.不过这界面做的确实还可以.运行此游 ...

  7. 《C++primer》v5 第2章 变量和基本类型 读书笔记 习题答案

    2.1 int,long long ,short 可表示范围和占用内存空间不同.具体与计算机有关. 无符号类型只能表示0和正数,带符号类型可以表示负数,0,正数. float是单精度,一般占用4个字节 ...

  8. SVD小结

    1.矩阵分解 假设一个矩阵Data是m行n列,SVD(奇异值分解)将Data分解为U,E,VT 三个矩阵: Datam*n=Um*kEk*kVTk*n E是一个对角矩阵,对角元素为奇异值,对应Data ...

  9. Windows Server 2008(R2)配置apache+php+mysql环境问题事项

    服务器环境:Windows 2008 R2 64位.apache,mysql,php都是32位. 1. 80端口的外网访问问题 表现:80端口本地可以访问,外网不能访问,换了8080端口也是一样,检查 ...

  10. java中,去除空白的方法

    有时候,我们页面传过来的值,或者做excel导入时填入的值都需要去掉像空格一样的一些特殊字符,下面这个方法可去掉像制表符,换行键,回车,空格或者不在ACSII中 的特殊字符 /** * 去除字符串开始 ...