Tornado 从入门到精通(二)下:协程调用方式与经典使用模式

前言

书接上回,在上一篇中我们详细讲解了 Tornado 协程的核心定义、原生协程与装饰器式协程的区别,以及底层运行原理。本篇我们将聚焦协程的正确调用规范高频经典使用模式,帮大家避开异步调用的常见陷阱,充分发挥协程的并发性能。


一、协程的正确调用方式

1.1 调用的核心原理

协程的异常传播机制和普通函数有本质区别: 协程内部产生的异常会被封存到可等待对象中,只有通过 yield / await 触发等待时,异常才会真正抛出。如果调用方式错误,异常会被静默隐藏,极难排查定位。

我们先看一个典型的错误调用示例:

bash 复制代码
async def divide(x, y):
    return x / y

def bad_call():
    # 此处本应触发 ZeroDivisionError,但因协程调用方式错误,异常永远不会暴露
    divide(1, 0)

问题说明 :在普通同步函数中直接调用 async 函数,只会生成一个协程对象,不会真正执行函数内部逻辑,代码既不会运行,异常也不会抛出。

1.2 标准调用规则

绝大多数场景下,调用协程的函数自身也必须是协程 ,通过 await(原生 async def 协程)或 yield(装饰器旧式协程)等待执行。

注意:重写 Tornado 框架的父类方法前,需要查阅官方文档确认该方法是否支持协程 ------ 只有文档标注「可为协程 / 可返回 Future」的方法,才可以改写为协程版本。

标准正确写法示例:

bash 复制代码
async def good_call():
    # await 会解包 divide() 返回的可等待对象,正常抛出异常
    await divide(1, 0)

1.3 场景 1:即发即忘(无需等待执行结果)

如果不需要获取协程的返回结果,只希望任务在后台异步执行,推荐使用 IOLoop.spawn_callback 将任务交由事件循环托管。 这种方式的优势是:任务执行失败时,事件循环会自动打印堆栈日志,不会出现异常静默丢失的问题。

bash 复制代码
# IOLoop 会捕获异常并在日志中输出堆栈信息
# 注意这里传入的是函数对象,由 IOLoop 负责调用执行
IOLoop.current().spawn_callback(divide, 1, 0)

适配说明

  • 装饰器式协程(@gen.coroutine):推荐使用该方法管理后台异步任务;
  • 原生协程(async def):必须使用该方法,否则协程调度器不会启动任务,代码永远不会执行。

1.4 场景 2:程序顶层入口(事件循环未启动时)

在脚本、批处理程序的主入口,事件循环尚未启动的场景下,使用 IOLoop.run_sync 来启动事件循环、运行指定协程,执行完毕后自动关闭事件循环。

注意:run_sync 不支持直接传入带参数的协程调用,需要用 lambda 包裹一层。

bash 复制代码
# run_sync() 不直接接收参数,因此需要用 lambda 包裹调用
IOLoop.current().run_sync(lambda: divide(1, 0))

二、协程经典使用模式

2.1 在协程中调用阻塞函数

协程运行在单线程事件循环中,不能直接执行耗时阻塞函数(如同步文件 IO、密集计算、同步数据库请求等),否则会卡住整个事件循环,导致所有并发任务阻塞。

最优解决方案:使用 IOLoop.run_in_executor,将阻塞函数投递到线程池中执行,返回兼容协程的 Future 对象,通过 await 异步等待结果。

bash 复制代码
async def call_blocking():
    await IOLoop.current().run_in_executor(None, blocking_func, args)

参数说明:第一个参数为线程池实例,传入 None 则使用 Tornado 默认线程池;后续参数为阻塞函数及其入参。

2.2 并行执行多个异步任务

当需要同时发起多个异步任务、等待全部完成后再继续执行时,可以使用 tornado.gen.multi。它支持传入列表、字典格式的 Future 集合,并行等待所有任务执行完毕,大幅提升批量任务的执行效率。

原生协程写法

bash 复制代码
from tornado.gen import multi

# 列表形式:按输入顺序返回对应结果
async def parallel_fetch(url1, url2):
    resp1, resp2 = await multi([
        http_client.fetch(url1),
        http_client.fetch(url2)
    ])

# 批量处理多个 url
async def parallel_fetch_many(urls):
    responses = await multi([http_client.fetch(url) for url in urls])
    # responses 是与输入顺序一一对应的 HTTPResponse 列表

# 字典形式:按 key 映射返回结果
async def parallel_fetch_dict(urls):
    responses = await multi({url: http_client.fetch(url) for url in urls})
    # responses 是 {url: HTTPResponse} 格式的字典

装饰器式协程写法

旧式 @gen.coroutine 装饰的协程支持更简洁的写法,直接 yield 列表或字典即可实现并行等待:

bash 复制代码
@gen.coroutine
def parallel_fetch_decorated(url1, url2):
    resp1, resp2 = yield [
        http_client.fetch(url1),
        http_client.fetch(url2)
    ]

2.3 穿插调度(流水线式执行)

核心思路 :不立刻 await 刚创建的任务,先保存 Future 对象、启动下一个任务,再分批等待结果,通过「计算 / IO」的穿插重叠提升整体吞吐量,特别适合流式数据处理场景。

1. 原生 async def 写法

原生协程需要借助 convert_yielded 在后台启动协程,作用等价于 asyncio.ensure_future(),二者在 Tornado 中均可使用。

bash 复制代码
from tornado.gen import convert_yielded

async def get(self):
    # convert_yielded() 会在后台启动原生协程
    fetch_future = convert_yielded(self.fetch_next_chunk())
    while True:
        chunk = await fetch_future
        if chunk is None:
            break
        self.write(chunk)
        # 等待当前数据写入的同时,预取下一块数据
        fetch_future = convert_yielded(self.fetch_next_chunk())
        await self.flush()

2. 旧式装饰器协程写法

装饰器式协程调用后会立即启动任务,因此写法更简洁,无需额外转换:

bash 复制代码
@gen.coroutine
def get(self):
    fetch_future = self.fetch_next_chunk()
    while True:
        chunk = yield fetch_future
        if chunk is None:
            break
        self.write(chunk)
        fetch_future = self.fetch_next_chunk()
        yield self.flush()

总结

协程的调用有严格的规范约束,错误的调用方式会导致代码不执行、异常静默丢失等隐蔽问题,是异步开发的高频踩坑点。日常开发中需要根据业务场景选择合适的调用方式,灵活运用并行执行、穿插调度等经典模式,才能充分发挥 Tornado 异步架构的性能优势。

到这里,Tornado 协程的核心概念、底层原理与实战用法就全部讲解完毕了,感兴趣的小伙伴可以结合实际业务场景动手实践。

相关推荐
雪碧透心凉_1 小时前
Python OpenCV图像处理入门
图像处理·python·opencv
李可以量化1 小时前
Tornado 从入门到精通(二)上:协程核心机制详解
python
晴天161 小时前
AgentLoop分享(上): 让 AI 真正“自主干活“-Day15
人工智能·python
Patrick在香港2 小时前
Python爬虫框架实战:优雅抓取香港政府公开API数据的通用方案
java·开发语言·爬虫·python·ai编程·数据可视化·可用性测试
李白的天不白2 小时前
python下载脚本
python
2501_909509102 小时前
DAY 45
python
郝学胜_神的一滴2 小时前
Python 高级编程 027:四大推导式全攻略
python·pycharm
菜冻鱼2 小时前
Python-sklearn-特征工程
开发语言·人工智能·python·机器学习·numpy·matplotlib·sklearn
mit6.8243 小时前
archived
后端·python·flask