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. 代码执行流程(无结果时)

 

  1. 调用 await future

    • 协程挂起,Tornado 事件循环继续运行。

  2. 后台轮询

    • _on_result 每 10ms 检查一次 Celery 任务状态(通过 call_later)。

  3. 超时或完成

    • 若任务完成: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 是异步等待:挂起当前协程但不阻塞线程,超时或完成后自动恢复。

  • 真正的阻塞只有两种情况:

    1. 错误使用同步方法(如 future.result())。

    2. 外部依赖(如 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 时:

 

  1. 协程挂起(Suspend):

    • 当前协程(如 ExecuteHandler.post)的执行被暂停,代码停留在 await 行。

    • 线程立即释放,事件循环可以处理其他协程或 I/O 事件。

    • 例如:此时其他 HTTP 请求仍能被正常处理。

  2. 恢复条件:

    • 任务完成:_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. 结合上下文的完整流程

 

  1. 请求进入 post 方法:

    • 创建 future 并启动 Celery 任务(async_fetch)。

    • future 初始状态为 Pending

  2. 执行 await future

    • 协程挂起,Tornado 处理其他请求。

    • _on_result 在后台轮询 Celery 任务状态(非阻塞)。

  3. 任务完成或超时:

    • 完成:_on_result 调用 future.set_result(),协程恢复,script_rs 获取结果。

    • 超时:future.set_exception() 抛出超时异常。

  4. 返回响应:

    • 协程恢复后,继续执行并返回 render_json

 


 

4. 为什么需要挂起协程?

 

  • 高并发基石:
    通过挂起/恢复机制,单线程可高效处理大量并发请求(如 C10K 问题)。

  • 资源友好:
    避免为每个请求创建线程或进程,减少上下文切换开销。

 


 

5. 常见误区

 

  • 误区:await 是“非阻塞”的,所以不会等待。
    正解:await 会等待当前协程依赖的任务完成,但不阻塞线程。

  • 误区:协程挂起时任务在后台“自动运行”。
    正解:任务是否运行取决于 Celery Worker,Tornado 只是轮询状态。

 


 

总结

 

    • Future 状态:Pending → Done/Cancelled

    • 协程挂起:暂停当前任务链,释放线程,由事件循环驱动异步恢复。

    • 设计优势:用同步代码写法实现异步高性能,避免回调地狱。

 

 
 
posted @ 2025-06-25 14:12  郭慕荣  阅读(12)  评论(0)    收藏  举报