异步函数中上下文绑定问题的核心是协程切换导致传统全局变量或threading.local无法隔离数据,解决关键是每个任务独有一份且自动传递;应使用contextvars.contextvar规范声明与设值,注意default设置、协程内.set()、子任务上下文继承及避免跨线程/非event loop场景导致的上下文丢失。

异步函数中上下文绑定问题的核心是:协程切换时,传统全局变量或 threading.local 无法隔离数据,导致请求 ID、用户身份、日志追踪等信息错乱或丢失。解决的关键不是“共享”,而是“每个任务独有一份且自动传递”。
用 contextvars 声明并安全设值
contextvars.ContextVar 是 Python 标准方案,但必须规范使用:
- 声明时务必指定
default,例如request_id = ContextVar("request_id", default="");否则调用.get()会直接抛LookupError - 设值只能在协程内通过
.set(),且该值只对当前协程及其await链生效;create_task()启动的新任务默认不继承上下文 - 若需子任务携带父上下文,Python 3.11+ 可传
context=copy_context()给create_task();低版本推荐用ctx.run(sub_coro)包装后提交任务
避免上下文意外中断的常见场景
很多看似正常的写法会导致上下文“断掉”,.get() 总返回 default:
- 调用了
loop.run_in_executor或concurrent.futures进入线程池——此时已脱离 asyncio 上下文 - 在信号处理器(
signal.signal)、atexit回调或非 event loop 线程中访问 contextvar - 在 FastAPI 的同步路由(
def视图)里调异步函数,底层经 anyio 跨线程调度,上下文不会自动穿透
配合 async with 管理资源上下文
异步资源(如数据库连接、HTTP 会话)需用异步上下文管理器,不能复用同步版:
- 类中实现
async __aenter__和async __aexit__,两个方法都必须await所有异步操作 - 禁止在
__aexit__中调用同步 I/O(如file.close()),应换为await file.aclose() - 推荐优先使用
@asynccontextmanager装饰器,逻辑清晰、不易漏写await
日志与中间件中的上下文注入
要让每条日志带上 request_id,不能靠全局 logger 配置:
- 在请求入口(如 FastAPI 中间件)设好
request_id.set(xxx) - 自定义
logging.Filter,在filter(record)中调request_id.get()并写入record.request_id - 确保日志格式字符串包含
%(request_id)s,且整个链路未跨出协程上下文











