异步和非阻塞 I/O¶
实时 Web 功能需要每个用户保持长时间连接,且大部分时间处于空闲状态。在传统的同步 Web 服务器中,这意味着为每个用户分配一个线程,这可能非常昂贵。
为了最大程度地降低并发连接的成本,Tornado 使用单线程事件循环。这意味着所有应用程序代码都应以异步和非阻塞的方式运行,因为一次只能执行一个操作。
异步和非阻塞这两个术语密切相关,通常可以互换使用,但它们并非完全相同。
阻塞¶
当函数在返回之前等待某件事发生时,它就会阻塞。函数可能由于多种原因而阻塞:网络 I/O、磁盘 I/O、互斥锁等。实际上,每个函数都会阻塞,至少在它运行和使用 CPU 时会有一点阻塞(为了说明 CPU 阻塞为什么与其他类型的阻塞一样重要,请考虑像 bcrypt 这样的密码散列函数,它们的设计意图是使用数百毫秒的 CPU 时间,远远超过典型的网络或磁盘访问)。
函数在某些方面可能阻塞,而在其他方面可能不阻塞。在 Tornado 的上下文中,我们通常在网络 I/O 的背景下谈论阻塞,尽管所有类型的阻塞都应尽量减少。
异步¶
异步函数在完成之前返回,并且通常会导致一些工作在后台发生,然后在应用程序中触发一些未来的动作(与普通的同步函数不同,后者在返回之前会完成所有操作)。异步接口有许多种风格
回调参数
返回一个占位符(
Future
、Promise
、Deferred
)传递到队列
回调注册表(例如 POSIX 信号)
无论使用哪种类型的接口,异步函数定义上与调用者的交互方式不同;没有免费的方式来使同步函数以对调用者透明的方式变为异步(像 gevent 这样的系统使用轻量级线程来提供与异步系统相当的性能,但它们并没有真正使事情异步)。
Tornado 中的异步操作通常返回占位符对象(Futures
),除了 IOLoop
等一些低级组件使用回调。 Futures
通常使用 await
或 yield
关键字转换为其结果。
示例¶
这是一个同步函数示例
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
块在协程中按预期工作,而用回调实现这一点很困难。协程将在本指南的下一部分中深入讨论。