当前位置:首页 > 文章列表 > 文章 > python教程 > Python asyncio 怎样用绝对时间设置超时

Python asyncio 怎样用绝对时间设置超时

来源:17golang原创 2026-10-18 01:35:11 0浏览 收藏

在异步服务里,“最多等待 2 秒”和“必须在某个截止点前结束”不是同一个约束。asyncio.timeout(2) 接收相对时长,asyncio.timeout_at(deadline) 接收事件循环时钟上的绝对时间点。需要让一次请求的数据库、缓存和 HTTP 子操作共享总预算时,优先在入口计算一个 deadline,再向下传递。

官方地址:https://docs.python.org/3.14/library/asyncio-task.html

最稳妥的写法是:通过 asyncio.get_running_loop().time() 取得单调时钟,使用“当前时间 + 总预算”计算 deadline,再用 asyncio.timeout_at(deadline) 包住需要受约束的协程。TimeoutError 放在上下文管理器外捕获。

先分清 timeout 和 timeout_at 的输入含义

timeout(delay) 的参数是“从现在起再等多少秒”;timeout_at(when) 的参数则是“事件循环时钟走到哪个值时停止等待”。两者底层都属于 asyncio 的异步超时上下文管理器,但表达的约束层级不同。

写法参数语义适合的场景
asyncio.timeout(2)本层从进入上下文开始最多等待 2 秒独立的小操作,有清晰的局部预算
asyncio.timeout_at(deadline)在事件循环时钟到达 deadline 时结束多个嵌套操作共享一次请求的总截止时间
asyncio.wait_for(aw, 2)只约束一个 awaitable需要包住单个调用,不需要上下文范围时

这里的绝对时间不是 Unix 时间戳,也不应该用 datetime.now() 直接计算。asyncio 的 deadline 由事件循环的时钟解释,和 loop.time() 使用同一个时钟域,才能避免系统墙上时钟调整影响剩余时间。

用 loop.time() 计算一个可传递的 deadline

入口通常只做一次时间预算换算:读取当前事件循环时间,再加上本次请求允许的秒数。之后下层函数只接收 deadline,不再各自从当前时间重新加固定秒数。这样,调用链越深,剩余时间越少,约束仍然指向同一个截止点。

业务入口通过 loop.time 和相对预算计算 deadline 并连接到 asyncio.timeout_at 与 TimeoutError 的静态结构图
图1:以事件循环的单调时钟计算 deadline,再交给 timeout_at 作为静态超时边界;这是结构说明图,不是运行截图。
import asyncio


async def load_profile(deadline: float) -> dict:
    # 子函数接收同一时间域中的 deadline,不重复分配固定秒数。
    async with asyncio.timeout_at(deadline):
        await asyncio.sleep(0.05)
        return {"name": "示例用户"}


async def handle_request() -> dict | None:
    loop = asyncio.get_running_loop()
    # loop.time() 是事件循环的单调时钟;deadline 不是 Unix 时间戳。
    deadline = loop.time() + 2.0

    try:
        return await load_profile(deadline)
    except TimeoutError:
        # timeout_at 会在上下文退出时把取消转换成 TimeoutError。
        return None


asyncio.run(handle_request())

这个例子里,load_profile() 不需要知道入口给了多少秒,也不需要重新调用 loop.time()。它只关心传入的截止点。入口负责把超时转换成业务结果,子函数可以继续向更深层传递同一个值。

TimeoutError 要放在上下文管理器外面捕获

asyncio.timeout() 和 asyncio.timeout_at() 在超时时会取消当前任务,并在上下文管理器退出时把取消转换为内置的 TimeoutError。因此,把 try/except TimeoutError 写在 async with 外层更可靠;在上下文内部捕获这个异常,位置通常还没有完成转换。

import asyncio


async def fetch_slow_data() -> str:
    # 这里只模拟一个可能超过截止点的异步操作,不表示真实网络结果。
    await asyncio.sleep(3)
    return "data"


async def main() -> None:
    deadline = asyncio.get_running_loop().time() + 1.0
    try:
        async with asyncio.timeout_at(deadline):
            # 超时取消的是当前任务;资源清理仍应由被调用代码负责。
            value = await fetch_slow_data()
            print(value)
    except TimeoutError:
        # 异常在 async with 退出后处理,适合映射为超时响应或重试信号。
        print("请求超过统一截止时间")


asyncio.run(main())

不要把 TimeoutError 当成“后台任务一定停止”的证明。它表示当前超时上下文已经结束;被取消的协程仍应在自己的 finally 中关闭连接、释放锁或撤销临时资源。

把剩余预算传给嵌套的数据库和 HTTP 操作

固定给数据库 2 秒、再固定给 HTTP 2 秒,看似简单,却可能让一次请求总共等待 4 秒,超过入口原本允许的 2 秒。共享 deadline 的做法是让每一层都在同一时间点上工作,必要时把剩余时间换成局部的 timeout(remaining)。

外层请求共享总 deadline 和剩余预算并约束数据库查询、HTTP 调用的静态结构图
图2:同一个总 deadline 约束多个异步子操作,剩余预算由调用层共享;这是静态关系图,不代表实际执行顺序。
import asyncio


async def query_database(remaining: float) -> list[dict]:
    # 局部调用只使用上层传来的剩余预算,避免重新开一份总预算。
    async with asyncio.timeout(remaining):
        await asyncio.sleep(0.02)
        return [{"id": 1}]


async def call_service(remaining: float) -> str:
    # 外部服务也受同一个请求预算约束,实际客户端还应处理连接清理。
    async with asyncio.timeout(remaining):
        await asyncio.sleep(0.02)
        return "ok"


async def compose_result() -> tuple[list[dict], str] | None:
    loop = asyncio.get_running_loop()
    deadline = loop.time() + 1.0

    async def remaining_budget() -> float:
        # max 保证传入 timeout 的值不因时钟精度出现负数。
        return max(0.0, deadline - loop.time())

    try:
        rows = await query_database(await remaining_budget())
        state = await call_service(await remaining_budget())
        return rows, state
    except TimeoutError:
        # 任何一层耗尽同一 deadline,都统一从入口返回超时结果。
        return None


asyncio.run(compose_result())

如果两个子操作本来应该并发执行,可以把同一个 deadline 传给两个任务,再在外层统一处理异常;不要把上层 deadline 当成两个可以相加的独立预算。是否并发属于业务取舍,绝对截止点本身只表达时间边界。

不知道 deadline 时,用 timeout(None) 再 reschedule

有些流程在进入上下文时还拿不到上游的截止点,例如先读取一段配置,再根据配置计算剩余时间。此时可以用 asyncio.timeout(None) 创建一个暂不触发的上下文,拿到时间点后调用上下文对象的 reschedule()。

import asyncio


async def prepare_and_run() -> None:
    loop = asyncio.get_running_loop()
    try:
        # None 表示当前不设置截止点,后续可以重新安排。
        async with asyncio.timeout(None) as timeout_context:
            await asyncio.sleep(0)
            deadline = loop.time() + 1.0
            # reschedule 使用的仍然是 loop.time() 所在的时间域。
            timeout_context.reschedule(deadline)
            await asyncio.sleep(0.1)
    except TimeoutError:
        # 超时异常依然在上下文管理器外处理。
        print("准备阶段之后的操作超过 deadline")


asyncio.run(prepare_and_run())

Timeout 对象还可以通过 when() 查看当前 deadline,通过 expired() 判断上下文是否已经超过截止点。它们适合记录边界状态或做业务分支,但不应替代对 TimeoutError 的正常处理。

如果传入的 when 已经早于当前 loop.time(),官方语义是超时会在事件循环的下一次迭代触发。这个行为很适合表示“预算已经耗尽”,但调用方要预期到被包住的代码几乎不会获得额外执行时间。

什么时候改用 wait_for

asyncio.wait_for(awaitable, timeout) 适合单个 awaitable 的局部等待。它接收的是相对秒数;超时后会取消被等待对象,并抛出 TimeoutError。如果调用层已经有统一 deadline,先计算 remaining = max(0, deadline - loop.time()),再把剩余值交给 wait_for,比每一层都写一个固定秒数更一致。

import asyncio


async def one_operation() -> str:
    # 单个 awaitable 有独立封装需求时,wait_for 更直观。
    await asyncio.sleep(0.1)
    return "done"


async def call_one(deadline: float) -> str | None:
    loop = asyncio.get_running_loop()
    # 把绝对 deadline 换算成当前调用还能使用的相对预算。
    remaining = max(0.0, deadline - loop.time())
    try:
        return await asyncio.wait_for(one_operation(), timeout=remaining)
    except TimeoutError:
        # wait_for 的超时也是从调用处抛出,适合单点兜底。
        return None


async def main() -> None:
    loop = asyncio.get_running_loop()
    # deadline 必须在运行中的事件循环里计算,再传给单点调用。
    result = await call_one(loop.time() + 1.0)
    print(result)


asyncio.run(main())

在实际程序中不要手动创建或保存事件循环对象,应该在运行中的协程内用 get_running_loop() 计算 deadline,再调用 call_one()。若需要包住多个 await、嵌套上下文或统一处理取消,timeout_at() 通常更能表达设计意图。

常见误区与选择清单

  • 不要把 datetime.now().timestamp() 当成 timeout_at() 的参数;这里要用 loop.time() 的时间域。
  • 不要在每个下层函数里重新执行“当前时间 + 固定秒数”;这会让总耗时逐层累加。
  • 不要在 async with 内部等待 TimeoutError;把异常处理放到上下文外。
  • 需要动态获得时间点时,先用 timeout(None),拿到 deadline 后再 reschedule()。
  • 只包一个 awaitable 时可以选 wait_for();要表达请求范围或嵌套关系时选 timeout_at()。

结语

绝对 deadline 的价值不在于语法更复杂,而在于把“请求必须在什么时候结束”变成可传递的约束。入口用 loop.time() + budget 建立时间点,下层复用它,异常在超时上下文外统一处理;这样数据库、缓存和服务调用不会各自偷偷扩张总预算。

相关问题

timeout_at 的 when 能不能传 Unix 时间戳?

不能直接这样做。when 应来自事件循环的时钟域,常见写法是 loop.time() + seconds。Unix 时间戳属于另一套墙上时钟。

timeout_at 超时后应该捕获 asyncio.TimeoutError 吗?

现代 Python 文档使用内置 TimeoutError。重点是把捕获位置放在 async with 外,而不是把异常处理写进超时上下文内部。

timeout 和 timeout_at 哪个更快?

两者不是性能选型题。按是否需要共享一次请求的绝对截止点选择:局部等待可用 timeout,共享总预算可用 timeout_at。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Python SharedMemory 的 close 和 unlink 有什么区别Python SharedMemory 的 close 和 unlink 有什么区别
上一篇
Python SharedMemory 的 close 和 unlink 有什么区别
Python asyncio 怎样把阻塞函数放到线程执行
下一篇
Python asyncio 怎样把阻塞函数放到线程执行
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    510次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    570次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    596次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    548次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    364次使用