script_rs = await future 如果没有结果 调用者会一直被阻塞吗
import json from .base import BaseHandler class Handler(BaseHandler): """For health check. """ def get(self): self.render_json({'status': 'UP'}) async def post(self): try: self.log.debug(f"Request body: {self.request.body.decode()}") body = json.loads(self.request.body) except Exception: raise Exception("Bad request body.") task_id = body['id'] self.log.debug(f"Task ID: {task_id}") version = body.get('lanVersion', 'py385') future = self.async_fetch('executor.pyexec', body, queue=version) task_id = future.task_id self.log.debug(f"Request script body[{body['id']}][{task_id}]: {body['scriptContent']}") script_rs = await future self.log.debug(f"script_rs type:{type(script_rs)},, future type:{type(future)}") self.log.debug(f"future {future}") self.log.debug(f"script_rs {script_rs}") self.log.debug(f"Response body[{body['id']}][{task_id}]: {script_rs}") result = { 'code': 0, 'data': { 'id': body['id'], 'seq_no': task_id, 'result': script_rs } } self.render_json(result)
结合上下文分析 script_rs = await future 这行代码 如果没有结果 调用者会一直被阻塞吗?
1. 核心结论
await future 不会阻塞整个 Tornado 服务器或线程,但会挂起当前协程(coroutine),直到 future 完成或超时。这是异步编程中的“非阻塞等待”,与同步阻塞有本质区别。
2. 关键机制解析
(1) Tornado 的协程模型
-
协程挂起(Suspend)≠ 线程阻塞(Block)
-
当执行
await future时,当前协程会暂停执行,但 Tornado 的事件循环(IOLoop)会继续处理其他请求。 -
一旦
future通过set_result()被填充,挂起的协程会恢复执行。
-
(2) future 的结果来源
在 BaseHandler.async_fetch 中:
future = CeleryFuture()
IOLoop.instance().add_callback(self._on_result, result, future) # 异步轮询
return future
-
_on_result会定期检查 Celery 任务状态(通过result.ready()),完成后调用future.set_result()。 -
若任务未完成:
_on_result会通过call_later延迟重试(非阻塞)。
(3) 超时控制
在 _on_result 中定义的超时逻辑:
async_timeout = self.settings.get("async_timeout", 2) # 默认2秒超时
if (now - self.start) >= async_timeout:
future.set_exception(Exception("Async task timeout."))
-
如果任务超过
async_timeout未完成,future会抛出超时异常,避免无限等待。
3. 阻塞场景对比
| 场景 | 行为 | 是否影响其他请求 |
|---|---|---|
await future |
挂起当前协程,释放线程给其他任务 | ❌ 不影响 |
同步 future.result() |
强制阻塞线程,直到结果返回(错误用法!) | ✅ 阻塞整个线程 |
| Celery 任务卡死 | _on_result 超时触发 set_exception,协程恢复并报错 |
❌ 不影响 |
4. 代码执行流程(无结果时)
-
调用
await future-
协程挂起,Tornado 事件循环继续运行。
-
-
后台轮询
-
_on_result每 10ms 检查一次 Celery 任务状态(通过call_later)。
-
-
超时或完成
-
若任务完成:
future.set_result()唤醒协程,继续执行后续代码。 -
若超时(默认2秒):
future.set_exception()抛出超时异常。
-
5. 为什么不会真正“阻塞”?
-
Tornado 的单线程异步模型:
IOLoop 通过协程挂起和事件回调实现高并发,await只是暂停当前任务链,线程仍可处理其他请求。 -
Celery 的异步协作:
通过_on_result的延迟轮询(非阻塞)替代同步等待。
6. 验证实验
(1) 模拟长时间 Celery 任务
# Celery 任务(模拟耗时操作)
@app.task
def pyexec(body):
time.sleep(10) # 10秒后才返回
return {"result": "done"}
(2) 观察行为
-
客户端请求:
-
2秒后(默认超时)收到
Async task timeout错误。
-
-
服务端日志:
-
其他请求仍可正常处理,无线程阻塞。
-
7. 总结
-
await future是异步等待:挂起当前协程但不阻塞线程,超时或完成后自动恢复。 -
真正的阻塞只有两种情况:
-
错误使用同步方法(如
future.result())。 -
外部依赖(如 Celery Worker)完全死锁且无超时机制。
-
-
生产建议:
-
合理设置
async_timeout(如 5~30 秒)。 -
监控 Celery 任务队列,避免积压。
-
结合上下文分析,future有几种状态? 挂起当前协程详细解释一下什么含义?
在 Tornado 和异步编程中,Future 对象(包括其子类 CeleryFuture)通常有以下几种状态:
| 状态 | 描述 | 触发条件 |
|---|---|---|
| Pending | 初始状态,表示任务尚未完成或未取消。 | Future 刚被创建时。 |
| Running | 任务正在执行(某些框架中可能没有显式区分此状态)。 | 任务开始执行但未完成。 |
| Done | 任务已完成(成功或失败)。 | 调用 set_result() 或 set_exception()。 |
| Cancelled | 任务被显式取消。 | 调用 cancel() 方法。 |
在代码中的体现
-
BaseHandler.async_fetch返回的future:
初始状态为Pending,后续由_on_result轮询 Celery 任务状态,完成后调用set_result()或set_exception()转为Done。 -
状态检查方法:
future.done() # 返回 True 表示任务完成(成功或失败) future.result() # 获取结果(若未完成会阻塞,但异步编程中应避免直接调用)
2. "挂起当前协程" 的详细解释
(1) 什么是协程(Coroutine)?
协程是轻量级的用户态线程,由事件循环(如 Tornado 的 IOLoop)调度。
-
特点:
-
可以在任意位置暂停(
yield或await)和恢复。 -
不阻塞操作系统线程,一个线程可并发处理数千个协程。
-
(2) await future 的具体行为
当执行 script_rs = await future 时:
-
协程挂起(Suspend):
-
当前协程(如
ExecuteHandler.post)的执行被暂停,代码停留在await行。 -
线程立即释放,事件循环可以处理其他协程或 I/O 事件。
-
例如:此时其他 HTTP 请求仍能被正常处理。
-
-
恢复条件:
-
任务完成:
_on_result调用future.set_result(data),事件循环将协程重新加入队列,从await处恢复执行,script_rs被赋值为结果。 -
任务超时或失败:
future.set_exception()触发,协程恢复并抛出异常。
-
(3) 与同步阻塞的对比
| 行为 | 同步阻塞(如 time.sleep) | 异步挂起(如 await future) |
|---|---|---|
| 线程状态 | 线程被占用,无法处理其他任务。 | 线程立即释放,处理其他协程。 |
| 资源消耗 | 每个阻塞操作需一个线程,高并发时资源爆炸。 | 单线程可处理数千协程。 |
| 典型场景 | 直接调用 result.get()。 |
使用 await 或 yield。 |
(4) 代码示例说明
async def post(self):
future = self.async_fetch() # 返回 Pending 状态的 future
script_rs = await future # 协程挂起,直到 future 完成
# 恢复后继续执行
self.render_json(script_rs)
-
若
future未完成:await行暂停,Tornado 处理其他请求。 -
若
future完成:事件循环从await行恢复,继续执行render_json。
3. 结合上下文的完整流程
-
请求进入
post方法:-
创建
future并启动 Celery 任务(async_fetch)。 -
future初始状态为Pending。
-
-
执行
await future:-
协程挂起,Tornado 处理其他请求。
-
_on_result在后台轮询 Celery 任务状态(非阻塞)。
-
-
任务完成或超时:
-
完成:
_on_result调用future.set_result(),协程恢复,script_rs获取结果。 -
超时:
future.set_exception()抛出超时异常。
-
-
返回响应:
-
协程恢复后,继续执行并返回
render_json。
-
4. 为什么需要挂起协程?
-
高并发基石:
通过挂起/恢复机制,单线程可高效处理大量并发请求(如 C10K 问题)。 -
资源友好:
避免为每个请求创建线程或进程,减少上下文切换开销。
5. 常见误区
-
误区:
await是“非阻塞”的,所以不会等待。
正解:await会等待当前协程依赖的任务完成,但不阻塞线程。 -
误区:协程挂起时任务在后台“自动运行”。
正解:任务是否运行取决于 Celery Worker,Tornado 只是轮询状态。
总结
-
Future状态:Pending→Done/Cancelled。 -
协程挂起:暂停当前任务链,释放线程,由事件循环驱动异步恢复。
-
设计优势:用同步代码写法实现异步高性能,避免回调地狱。

浙公网安备 33010602011771号