Tornado:异步与非阻塞I/O

实时WEB应用(Real-time web)的特点是需要与用户建立长连接,在一个传统的同步WEB服务中这意味这为每个用户单独开一个线程,这是很耗能的。

为了尽量减少并发数量,Tornado使用单进程事件循环,这意味这所有应用代码都应该是异步与非阻塞的,因为同一个时间只能有一个操作。

异步与非阻塞紧密相关,而且通常是可交替的,但它们并不是同一个东西。

阻塞

当一个函数在返回前等待一些东西出现的时候便会发生**阻塞**,函数阻塞可能有多个原因: 网络I/O, 硬盘I/O, 互斥器(mutexes)等. 事实上所有函数,至少大部分在运行及使用CPU都会出现阻塞 (举一个极端的例子,演示了为什么 CPU 阻塞必须作为其他种类的阻塞,请考虑密码散列函数像 bcrypt, 设计使用几百毫秒CPU时间,远远超过典型的网络或磁盘访问)。

函数可能在某些情况阻塞而其他情况非阻塞,比如 tornado.httpclient 在默认配置的情况下阻塞DNS解析,但不阻塞其他网络入口 (为减缓该情况可以使用带正确配置的 libcurlThreadedResolvertornado.curl_httpclient )。在Tornado上下文中我们主要介绍网络I/O阻塞,虽然所有的阻塞都有所减缓。

异步

异步 函数在完成前已经返回,应用程序通常触发操作前便可以使某些工作在后台完成(与 同步 函数恰好相反, 返回去必须将所有事情处理完),有许多异步接口风格:

  • 回调参数
  • 返回一个占位符 (Future, Promise, Deferred)
  • 转交给队列
  • 回调注册 (比如POSIX信号)

无论使用哪种类型的接口, 通过定义 异步函数以不同的方式与它们的调用者交互,同步函数无法通过异步方式与它的调用者交互(像 gevent 使用轻量级线程提供异步系统相媲美的性能,但他们做不实际上使事情异步系统).

示例

这是一个同步函数示例:

from tornado.httpclient import HTTPClient

def synchronous_fetch(url):
    http_client = HTTPClient()
    response = http_client.fetch(url)
    return response.body

下面是通过回调的异步方式:

from tornado.httpclient import AsyncHTTPClient

def asynchronous_fetch(url, callback):
    http_client = AsyncHTTPClient()
    def handle_response(response):
        callback(response.body)
    http_client.fetch(url, callback=handle_response)

接着是 Future 取代了回调:

from tornado.concurrent import Future

def async_fetch_future(url):
    http_client = AsyncHTTPClient()
    my_future = Future()
    fetch_future = http_client.fetch(url)
    fetch_future.add_done_callback(
        lambda f: my_future.set_result(f.result()))
    return my_future

原本的 Future 版本是更加复杂的, 但Tornado中 Futures 是被推荐使用的,因为它有两个主要优点。 错误处理是更加一致,congestion Future.result 方法能够简单的抛出异常(而不是回调接口中的异常), Futures 可以让自己与其他协同模块结合到一起。协同模块将在本指南的下一节中深入讨论。这里是我们的样本函数,非常类似于原始的同步版本的协程版本:

from tornado import gen

@gen.coroutine
def fetch_coroutine(url):
    http_client = AsyncHTTPClient()
    response = yield http_client.fetch(url)
    raise gen.Return(response.body)

raise gen.Return(response.body) 语句是 Python 2 (以及 3.2)常用到的, 通常不允许返回值。为了克服这个问题, Tornado协同模块抛出了一个名为 Return 的特殊异常。协同模块捕获异常并像返回值一样对待。在Python 3.3及更高版本中, return response.body 实现了相同的效果。