Tornado 0102 - 用户指南: 异步和非阻塞 I/O


异步和非阻塞 I/O(Asynchronous and non-Blocking I/O)

实时 Web 功能需要为每个用户长期保持大部分空闲连接。在传统的同步 Web 服务器中,这意味着要为每个用户投入一个线程,这成本可能非常昂贵。

为了最小化并发连接的成本,Tornado 使用单线程事件循环。这意味着所有应用程序代码都应该是异步和非阻塞的,因为一次只能有一个操作处于活动状态。

异步非阻塞 这两个术语是密切相关的,并且通常可以互换使用,但它们并不完全相同。

阻塞(Blocking)

函数在返回之前等待某事发生时会阻塞(blocks)。一个函数可能由于多种原因而阻塞:网络 I/O、磁盘 I/O、互斥锁等。事实上,每一个函数在运行和使用 CPU 时都会至少有一点阻塞(对于一个极端的例子来说明为什么 CPU 阻塞必须像其他类型的阻塞一样严肃,考虑密码散列函数,如 bcrypt ,它设计使用数百毫秒的 CPU 时间,远远超过典型的网络或磁盘访问)。

一个函数可能在某些方面是阻塞的,而在其他方面是非阻塞的。虽然各种阻塞都要尽量减少,但在 Tornado 的上下文中,我们通常情况是谈论在网络 I/O 上下文中的阻塞。

异步(Asynchronous)

异步函数在它执行完成之前就会返回,并且通常在触发应用程序中的一些未来操作之前,会将某些工作放到后台去执行(相反地,同步函数一定要等到执行完它所有操作才会返回)。异步接口有很多种风格:

  • 回调参数
  • 返回占位符(Future,Promise,Deferred)
  • 交付队列
  • 回调注册表(例如 POSIX 信号)

无论使用哪种类型的接口,定义的每种异步函数都需要让调用者使用不同的交互方式;没有一种毫无成本的方法可以使同步函数直接变成异步函数,却又对其调用者透明的(像 gevent 这样的系统使用轻量级线程来提供与异步系统相当的性能,但它们实际上并不会使事情变成异步)。

Tornado 中的异步操作通常返回占位符对象(Futures),但一些低级别组件(例如使用回调的IOLoop)除外。Futures 通常会通过 await 或 yield 关键字转换为结果。

Examples

这是一个同步函数示例:

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

async def asynchronous_fetch(url):
    http_client = AsyncHTTPClient()
    response = await http_client.fetch(url)
    return response.body

或者为了与旧版本的 Python 兼容,使用 tornado.gen 模块:

from tornado.httpclient import AsyncHTTPClient
from tornado import gen

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

协程有点神奇,但它的内部其实是这样做的:

from tornado.concurrent import Future

def async_fetch_manual(url):
    http_client = AsyncHTTPClient()
    my_future = Future()
    fetch_future = http_client.fetch(url)
    def on_fetch(f):
        my_future.set_result(f.result().body)
    fetch_future.add_done_callback(on_fetch)
    return my_future

请注意,协程在请求完成之前就返回其 Future。这就是使协程异步的原因。

你用协程所能做的任何事情你也可以通过传递回调对象来实现,但协程提供了很重要的简化,它让你可以使用与同步模式相同的方式组织代码。这对于错误处理尤为重要,因为 try / except 块的工作方式与协程中的预期相同,而回调则会很难实现。我们将在本指南的下一部分中对协程进行深入探讨。