代表性面试主题

编程面试:如何用 asyncio.timeout_at 和 reschedule 管理动态截止时间?

编程题困难
Offer.cc 编辑团队发布 更新

题干

一个异步请求只有在读取到上游元数据后才知道剩余预算。请使用 asyncio.timeout 或 timeout_at 设计动态截止时间,并解释 reschedule、expired、CancelledError、TimeoutError、嵌套上下文和 wait_for 的差异。

题干与适用场景

一个异步请求先读取路由元数据,之后才知道下游调用的剩余预算。实现要求:统一使用事件循环单调时钟,允许先创建无截止时间的上下文、随后设置绝对截止时间;超时后只能在上下文外处理 TimeoutError,调用方主动取消仍要继续传播。

Python 文档说明 asyncio.timeout() 返回可重排的异步上下文管理器,timeout_at() 接受事件循环时钟上的绝对时间。上下文内部的取消会在边界外转换为 TimeoutError

面试官考察点

考察候选人能否区分相对延迟与绝对截止时间、业务超时与外部取消;是否理解 reschedule()expired()、嵌套上下文和 wait_for() 的取消语义,并能给出共享预算与资源清理方案。

回答前需要澄清的问题

  • 预算由哪个组件拥有,是否已经消耗了读取元数据的时间?
  • 下游客户端、数据库和队列是否支持超时或取消?
  • 子调用需要共享一个绝对 deadline,还是各自拥有独立预算?
  • 超时是可降级结果、重试,还是直接返回错误?
  • 运行环境最低 Python 版本是否包含 asyncio.timeout

30 秒回答框架

“我用事件循环的 loop.time() 计算绝对 deadline。先用 asyncio.timeout(None) 建立作用域,读到元数据后调用 cm.reschedule(deadline);上下文内部发生取消,退出时会转换成 TimeoutError,所以只在外层捕获。外部 CancelledError 不转换,清理后继续抛出。下游 I/O 同样接收剩余预算,嵌套调用不能重新发放完整超时。需要观察 cm.expired() 区分到期和其他退出。”

分步骤深入解答

第一步:用单调时钟表达绝对截止时间

不要用墙上时钟计算剩余时间,因为系统校时会跳变。用 loop.time() 得到单调值,统一把 deadline 传给所有下游调用。

python
loop = asyncio.get_running_loop()
deadline = loop.time() + 2.0
async with asyncio.timeout_at(deadline):
    await call_dependency(deadline)

第二步:未知预算时先创建可重排上下文

当请求开始时还不知道预算,可用 asyncio.timeout(None) as cm。读取元数据后计算绝对 deadline,再调用 cm.reschedule(deadline);不要销毁上下文重建,避免中间工作失去统一边界。

第三步:把剩余预算传到下游

每个客户端根据 deadline - loop.time() 计算剩余秒数,并把非正值视为立即失败。这样路由、数据库和 HTTP 调用共享一个预算,不会层层重新计时。

第四步:理解取消到超时的转换

超时上下文在内部取消当前任务,并在上下文退出时将该取消转换为 TimeoutError。因此 TimeoutError 只能在 async with 外捕获;在内部捕获它会失效。

python
try:
    async with asyncio.timeout(1.0):
        await slow_call()
except TimeoutError:
    return degraded_result()

第五步:保留外部取消

请求被客户端断开或父任务取消时,CancelledError 不是业务超时。清理连接、锁和临时文件后继续抛出,不能把用户取消包装成降级成功。

第六步:比较 timeout、timeoutat 与 waitfor

timeout(delay) 使用相对秒数且支持重排;timeout_at(when) 直接使用绝对单调时间,适合跨多层传递 deadline。wait_for(aw, timeout) 针对一个 awaitable,超时会取消该 awaitable,并可能等待其完成取消,组合多个调用时更容易形成预算漂移。

第七步:处理嵌套和过期状态

超时上下文可以嵌套,内层应使用不晚于外层的 deadline。退出后通过 cm.expired() 判断是否真的到期;普通异常、外部取消和业务返回都不应被误报为 timeout。

第八步:验证竞态与清理

测试元数据迟到、deadline 已过、内层先超时、外部取消与超时同时发生、下游忽略取消、reschedule(None)、重复重排和清理失败。记录 deadline、剩余预算、取消原因和下游耗时,不记录敏感请求内容。

高质量示范回答

“我用 loop.time() 生成绝对 deadline,并把它传给所有下游。预算未知时先建立 timeout(None),元数据到达后 reschedule;内部取消由上下文在退出时转成外部 TimeoutError,所以只在上下文外捕获。外部 CancelledError 保持传播。嵌套调用共享最早 deadline,客户端和数据库也接收剩余预算。通过竞态测试确认无任务和资源泄漏。”

常见错误

  • time.time() 计算 deadline → 校时导致预算跳变 → 使用 loop.time()
  • 在 timeout 上下文内部捕获 TimeoutError → 捕获不到正确异常 → 在上下文外处理。
  • 把外部取消当超时 → 请求取消被错误降级 → 区分 CancelledError 与 TimeoutError。
  • 每层重新给完整 timeout → 总延迟超过契约 → 传递绝对 deadline。
  • 忽略 wait_for 的取消等待 → 实际耗时超过数字 → 评估取消收敛和驱动行为。
  • 只测成功和单一超时 → 竞态资源泄漏 → 覆盖同时取消、重排和清理失败。

追问及应对

追问一:为什么 timeout_at 比逐层 timeout 更稳定?

所有层都对同一个绝对时刻负责,不会因每层重新计算相对秒数而累积误差或超支。

追问二:reschedule 到过去的 deadline 会怎样?

上下文会在事件循环下一次机会触发超时;调用方应在重排前检查剩余预算并避免继续发起不可取消的 I/O。

追问三:如何让数据库也遵守 deadline?

将剩余秒数传给驱动的 statement timeout 或取消 API;Python task 被取消并不自动停止服务器端查询。

追问四:外部取消与 timeout 同时到达记录哪个?

保留取消计数和父任务原因,优先向上传播外部取消;把上下文是否 expired 作为诊断字段,而非把所有情况归为业务超时。

追问五:何时使用 wait_for?

单个 awaitable 需要简单相对超时时可用;跨多个子调用、需要动态截止时间或共享预算时,优先使用 timeout/timeout_at。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

截图题目后,按顺序看约束、解法、代码、边界条件和复杂度。

查看工具