Tornado 之所以能够处理高并发,核心原因在于它是一个异步非阻塞(Asynchronous and Non-blocking)的 Web 框架,这与传统的同步阻塞式框架(如 Flask/Django + Gunicorn/uWSGI)有着根本性的区别。
核心思想:异步非阻塞
让我们先理解这两个概念:
- 阻塞:当一个函数发起一个 I/O 操作(如网络请求、读写文件)时,如果这个操作需要时间,程序会“卡住”,等待操作完成才能继续执行下一步,在这期间,线程(或进程)处于空闲状态,无法处理其他请求。
- 非阻塞:当一个函数发起一个 I/O 操作时,它会立即返回,不会等待操作完成,程序可以继续执行其他任务,当 I/O 操作真正完成后,系统会通过某种机制通知程序来处理结果。
一个生动的比喻:
-
同步阻塞模型 (餐厅服务员)
- 一个服务员一次只服务一个客人。
- 他给客人点单后,就在厨房门口等,直到菜做好,端给客人,才能去服务下一个客人。
- 如果厨房很慢,这个服务员就完全被占用,其他客人在排队等待,餐厅效率很低。
-
异步非阻塞模型 (高效服务员)
- 一个服务员服务多个客人。
- 他给客人A点单后,立刻把菜单给厨房,然后转身去给客人B点单。
- 他会给厨房说:“A的菜好了叫我一下。”
- 在等待A的菜期间,他可以服务B、C、D。
- 当厨房通知“A的菜好了”,他才停下手中的活,去把菜端给A。
- 这样,服务员几乎没有空闲时间,餐厅效率极高。
Tornado 中的那个“高效服务员”就是单线程的事件循环。
Tornado 的三大支柱
Tornado 的并发能力建立在三个核心组件之上:
a. IOLoop - 事件循环
这是 Tornado 的心脏,是一个单线程的事件循环器,它的主要工作是:
- 监听所有注册的 I/O 事件(如 socket 是否可读、可写)。
- 当某个 I/O 事件发生时,执行对应的回调函数。
- 协调所有异步任务的执行顺序。
你可以把它想象成一个无穷的 while True 循环,不断地检查“有没有事情要做?”
b. AsyncHTTPClient - 异步 HTTP 客户端
Tornado 提供了强大的异步 HTTP 客户端,用于在 Tornado 应用内部发起异步的 HTTP 请求,当你用它去请求一个外部 API 时,发起请求后不会阻塞,而是会注册一个回调函数,当响应返回时,IOLoop 会负责调用这个回调函数。
c. tornado.gen - 协程
直接使用回调函数(Callback)会导致代码嵌套,形成所谓的“回调地狱”(Callback Hell),难以阅读和维护。
tornado.gen 模块提供了基于生成器的协程,让你能用看似同步的代码风格来编写异步逻辑,极大地提高了代码的可读性。
# 回调地狱 (不推荐)
def get_weather_callback(city, callback):
http_client = AsyncHTTPClient()
http_client.fetch("http://api.weather.com/" + city,
callback=lambda response: callback(json.loads(response.body)))
def get_news_callback(callback):
http_client = AsyncHTTPClient()
http_client.fetch("http://api.news.com/",
callback=lambda response: callback(json.loads(response.body)))
# 使用协程 (推荐)
from tornado import gen
from tornado.httpclient import AsyncHTTPClient
@gen.coroutine
def get_weather_coroutine(city):
http_client = AsyncHTTPClient()
response = yield http_client.fetch("http://api.weather.com/" + city)
# yield 暂停此函数,等待 fetch 完成
# fetch 完成后,IOLoop 会恢复此函数的执行
raise gen.Return(json.loads(response.body)) # Python 2.x 的返回方式
@gen.coroutine
def get_news_coroutine():
http_client = AsyncHTTPClient()
response = yield http_client.fetch("http://api.news.com/")
raise gen.Return(json.loads(response.body))
注意:在 Python 3.5+ 中,推荐使用
async/await语法,这是tornado.gen的超集,是官方推荐的方式。
实战示例:串行与并发的对比
假设我们有一个页面,需要同时获取用户信息、天气信息和新闻信息,在传统的同步框架中,这会非常慢。
同步方式(模拟) - 慢
import time
import requests # 同步库
def sync_get_user():
time.sleep(1) # 模拟耗时1秒
return {"user": "Alice"}
def sync_get_weather():
time.sleep(1) # 模拟耗时1秒
return {"weather": "Sunny"}
def sync_get_news():
time.sleep(1) # 模拟耗时1秒
return {"news": ["Tornado is fast", "Python 3.10 released"]}
# 总耗时 = 1 + 1 + 1 = 3 秒
start = time.time()
user = sync_get_user()
weather = sync_get_weather()
news = sync_get_news()
end = time.time()
print(f"Sync total time: {end - start:.2f} seconds")
Tornado 异步方式 - 快
import time
import tornado.ioloop
import tornado.web
import tornado.httpclient
from tornado import gen
# 模拟的异步API,使用 tornado.gen.sleep
@gen.coroutine
def async_get_user():
yield gen.sleep(1) # 非阻塞地等待1秒
return {"user": "Alice"}
@gen.coroutine
def async_get_weather():
yield gen.sleep(1)
return {"weather": "Sunny"}
@gen.coroutine
def async_get_news():
yield gen.sleep(1)
return {"news": ["Tornado is fast", "Python 3.10 released"]}
class MainHandler(tornado.web.RequestHandler):
@gen.coroutine
def get(self):
# 方式一:串行执行 (耗时约 3 秒)
# user = yield async_get_user()
# weather = yield async_get_weather()
# news = yield async_get_news()
# self.write(dict(user=user, weather=weather, news=news))
# 方式二:并发执行 (耗时约 1 秒) - 这是关键!
# tornado.gen.multi 会将多个协程“打包”,让 IOLoop 同时等待它们
# 当任何一个协程完成时,IOLoop 会去处理它,但总体上等待时间是所有协程中最长的那一个
user, weather, news = yield [async_get_user(), async_get_weather(), async_get_news()]
self.write(dict(user=user, weather=weather, news=news))
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
print("Server is running on http://localhost:8888")
tornado.ioloop.IOLoop.current().start()
分析方式二(并发执行):
yield [async_get_user(), async_get_weather(), async_get_news()]这行代码执行时,MainHandler会发起三个异步任务。IOLoop会注册这三个任务的完成事件,然后立即将控制权交还给事件循环,去处理其他连接或任务。- 1秒后,三个任务几乎同时完成。
IOLoop收到通知,恢复MainHandler的执行,将三个结果打包并返回给客户端。- 总耗时约 1 秒,而不是 3 秒,这就是 Tornado 高并发的威力所在。
如何选择异步库?
为了让 Tornado 的异步 I/O 生效,你使用的所有 I/O 库都必须是异步的,Tornado 官方提供了:
- 数据库:
torndb: Tornado 对 MySQLdb 的一个轻量级封装(仅支持 Python 2)。aiomysql: 基于asyncio的异步 MySQL 客户端(推荐,Python 3)。asyncpg: PostgreSQL 的异步驱动。aioredis: Redis 的异步客户端。
- HTTP 客户端:
tornado.httpclient.AsyncHTTPClient: Tornado 自带的,功能强大。
- 模板引擎:
tornado.template: Tornado 自带的模板引擎,本身是同步的,但由于渲染速度极快,在异步环境下通常不是瓶颈。
- 其他:
motor: MongoDB 的异步驱动。
绝对要避免:在 Tornado 的异步处理函数中直接使用 requests, time.sleep(), sqlite3 等同步库,这会阻塞整个事件循环,让 Tornado 退化成一个普通的同步服务器,失去所有并发优势。
Tornado vs. asyncio
Tornado 的 IOLoop 和 Python 标准库中的 asyncio 事件循环在功能上是相似的,Tornado 在早期(Python 3.4 之前)就实现了自己的异步模型,而 asyncio 是在 Python 3.4 才被引入标准库的。
关系:
- Tornado 的
IOLoop可以运行在asyncio事件循环之上。 - 你可以在 Tornado 应用中混合使用 Tornado 的协程 (
@gen.coroutine) 和asyncio的协程 (async def)。
如何结合使用:
import asyncio
import tornado.ioloop
import tornado.web
from tornado.platform.asyncio import AsyncIOMainLoop
# 将 Tornado 的 IOLoop 替换为 asyncio 的事件循环
AsyncIOMainLoop().install()
class AsyncioHandler(tornado.web.RequestHandler):
async def get(self):
# 可以直接使用 asyncio 的原生功能
print("Tornado handler is running on asyncio event loop")
await asyncio.sleep(1) # 这是 asyncio 的 sleep,非阻塞
self.write("Hello from Tornado + Asyncio!")
def make_app():
return tornado.web.Application([
(r"/asyncio", AsyncioHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
print("Server is running on http://localhost:8888 with asyncio loop")
# 现在启动的是 IOLoop,但它底层是 asyncio
tornado.ioloop.IOLoop.current().start()
| 特性 | 描述 |
|---|---|
| 核心模型 | 异步非阻塞 I/O,基于单线程事件循环。 |
| 高并发原理 | 遇到 I/O 操作时,不等待,注册回调,让出 CPU,继续处理其他请求,I/O 完成后由事件循环唤醒。 |
| 关键组件 | IOLoop (事件循环), AsyncHTTPClient (异步客户端), tornado.gen/async (协程语法糖)。 |
| 代码风格 | 使用 yield (旧版) 或 async/await (新版) 编写看似同步的异步代码,避免回调地狱。 |
| 性能瓶颈 | CPU 密集型任务,如果你的请求处理涉及大量计算(如视频编码、复杂算法),单线程的 Tornado 会成为瓶颈,因为计算会阻塞事件循环,此时应考虑使用多进程 (tornado.process) 或将计算任务交给其他服务(如 Celery)。 |
| 适用场景 | I/O 密集型应用,如长轮询、实时服务(聊天、游戏)、API 网关、需要同时处理大量客户端连接的场景。 |
掌握 Tornado 并发的关键在于理解其异步非阻塞的思想,并始终在异步的“轨道”上编写你的代码。
