ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CPython contextvars C API 全解:从 PyContext 类型对象到 3.14 上下文监视器

CPython contextvars C API 全解:从 PyContext 类型对象到 3.14 上下文监视器 CPython contextvars C API 全解从 PyContext 类型对象到 3.14 上下文监视器【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本文以 CPython 仓库中的 C API 文档Doc/c-api/contextvars.rst为主体系统讲解contextvars模块对应的公共 C APIPyContext、PyContextVar、PyContextToken三大类型对象、类型检查宏、上下文管理函数、3.14 新增的上下文监视器watcher以及PyContextVar_Get/Set/Reset等上下文变量操作函数的语义与内存模型。读完本文你可以在 C 扩展中直接创建并操作 context variables理解其不可变 HAMT 数据结构与线程本地上下文切换机制并利用上下文监视器回调实现跨语言的可观测性集成。一、contextvars C API 的来龙去脉contextvars模块于 Python 3.7 引入PEP 567用于管理上下文本地状态——与线程/任务绑定的、可安全跨asyncio任务边界传递的变量。C API 文档明确标注了versionadded:: 3.7并记录了一次重要的 API 断裂在 Python 3.7.1 中所有 context variables C API 的签名被修改改用PyObject指针替代PyContext、PyContextVar、PyContextToken等不透明结构体指针例如// in 3.7.0: PyContext *PyContext_New(void); // in 3.7.1: PyObject *PyContext_New(void);这一变更对应 issue 34762其动机是让 C API 遵循一切皆PyObject *的引用计数惯例。因此当前仓库中的头文件 Include/cpython/context.h 里所有函数均使用PyObject *参数与返回值。可用性前提从 Include/cpython/context.h 的第一行可以看到整个头文件被#ifndef Py_LIMITED_API包裹即该 C API 不属于稳定 ABILimited API扩展模块在未定义Py_LIMITED_API的常规构建下才能使用。二、核心类型体系结构、类型对象与检查宏文档定义了三个 C 结构体类型它们与 Python 层的contextvars.Context、contextvars.ContextVar、contextvars.Token一一对应C 类型对应的 Python 类语义PyContextcontextvars.Context一个上下文对象持有变量→值的映射PyContextVarcontextvars.ContextVar一个上下文变量声明式对象PyContextTokencontextvars.Tokenset()返回的回滚令牌头文件中的实际定义Include/cpython/context.h#L8-L20PyAPI_DATA(PyTypeObject) PyContext_Type; typedef struct _pycontextobject PyContext; PyAPI_DATA(PyTypeObject) PyContextVar_Type; typedef struct _pycontextvarobject PyContextVar; PyAPI_DATA(PyTypeObject) PyContextToken_Type; typedef struct _pycontexttokenobject PyContextToken; #define PyContext_CheckExact(o) Py_IS_TYPE((o), PyContext_Type) #define PyContextVar_CheckExact(o) Py_IS_TYPE((o), PyContextVar_Type) #define PyContextToken_CheckExact(o) Py_IS_TYPE((o), PyContextToken_Type)三个Py*_CheckExact检查宏要求参数非NULL且总是成功不会抛出异常因为它们只是简单的Py_IS_TYPE精确类型比较。2.1 内部结构布局从 Include/internal/pycore_context.h 的内部头文件可以看到三个结构体的完整布局该头仅供 CPython 内部构建使用扩展开发无需接触但理解它有助于理解 C API 语义struct _pycontextobject { PyObject_HEAD PyContext *ctx_prev; /* 前一个上下文用于退出时恢复 */ PyHamtObject *ctx_vars; /* 变量-值 的不可变 HAMT 映射 */ PyObject *ctx_weakreflist; int ctx_entered; /* 当前是否已处于进入状态 */ }; struct _pycontextvarobject { PyObject_HEAD PyObject *var_name; /* 变量名str */ PyObject *var_default; /* 默认值可为 NULL */ #ifndef Py_GIL_DISABLED PyObject *var_cached; /* 线程本地缓存 */ uint64_t var_cached_tsid; /* 缓存所属线程 ID */ uint64_t var_cached_tsver; /* 缓存写入时的上下文版本号 */ #endif Py_hash_t var_hash; }; struct _pycontexttokenobject { PyObject_HEAD PyContext *tok_ctx; /* 创建 token 时所在的上下文 */ PyContextVar *tok_var; /* 关联的 ContextVar */ PyObject *tok_oldval; /* set 之前的旧值NULL 表示此前无值 */ int tok_used; /* token 是否已被 reset 消耗 */ };几个关键事实可以从结构中读出Context的存储是一棵HAMTHash Array Mapped Trie持久化不可变哈希映射。PyContext_Copy之所以是浅拷贝是因为新上下文直接复用Py_NewRef同一棵 HAMT 树之后每次set都会基于旧树生成一棵新树旧树原样保留——这正是上下文快照天然并发安全的基础。PyContextVar在启用 GIL 的构建中带有线程本地缓存var_cached 线程 ID 上下文版本号PyContextVar_Get命中缓存时可跳过 HAMT 查找这是高频读取路径上的重要优化。PyContextToken记录创建时的上下文、关联变量和旧值且tok_used标志保证 token只能消费一次与 Python 层Token的行为一致。三、Context 对象管理函数3.1 创建与拷贝函数说明PyObject *PyContext_New(void)创建一个空上下文对象出错返回NULLPyObject *PyObject *PyContext_Copy(PyObject *ctx)创建传入上下文的浅拷贝出错返回NULLPyObject *PyContext_CopyCurrent(void)创建当前线程上下文的浅拷贝出错返回NULL实现位于 Python/context.c#L112-L140PyObject * PyContext_New(void) { return (PyObject *)context_new_empty(); } PyObject * PyContext_Copy(PyObject *octx) { ENSURE_Context(octx, NULL) PyContext *ctx (PyContext *)octx; PyHamtObject *vars context_get_vars(ctx); PyObject *res (PyObject *)context_new_from_vars(vars); Py_DECREF(vars); return res; }注意context_get_vars会在临界区critical section内对ctx_vars做Py_INCREF因此即使目标上下文正被其他线程作为当前上下文使用拷贝也是安全的。Python 层的contextvars.copy_context()正是直接转发到PyContext_CopyCurrent()见 Python/_contextvars.c#L15-L20。3.2 进入与退出上下文PyContext_Enter / PyContext_Exitint PyContext_Enter(PyObject *ctx); /* 成功 0失败 -1 */ int PyContext_Exit(PyObject *ctx); /* 成功 0失败 -1 */PyContext_Enter把ctx设为当前线程的当前上下文PyContext_Exit撤销它并恢复上一个上下文。从实现Python/context.c#L233-L302可以确认三个硬性约束C 扩展调用方必须遵守同一 Context 不能重入ctx_entered已置位时再Enter会抛RuntimeError: cannot enter context: ... is already entered注意这与 Python 层Context.run()的语义一致——run内部就是_PyContext_Enter 调用 _PyContext_Exit。必须先进入才能退出对未进入的上下文调用PyContext_Exit抛RuntimeError。退出必须与线程状态匹配若当前线程状态引用的不是该上下文对象只能有人误用 C API 时发生抛RuntimeError。进入/退出的核心操作是把当前上下文指针压栈/出栈int _PyContext_Enter(PyThreadState *ts, PyObject *octx) { ... ctx-ctx_prev (PyContext *)ts-context; /* borrow */ ts-context Py_NewRef(ctx); context_switched(ts); return 0; }ts-context是线程状态PyThreadState上的字段即当前上下文本身就是线程本地的ctx_prev链使得嵌套的 enter/exit 可以正确回退。3.3 懒初始化语义一个容易被忽略的细节新线程的ts-context初始为NULL。内部辅助函数context_get()Python/context.c#L522-L536会在第一次需要当前上下文时自动创建一个空上下文并挂到线程状态上。因此PyContextVar_Set/PyContextVar_Reset依赖context_get()首次 set 会自动建立上下文而PyContextVar_Get遇到ts-context NULL时不创建上下文直接走未找到分支返回默认值或把*value置NULL——这是有意的只读语义区分PyContext_Enter不经过context_get()所以在一个全新线程上直接 enter 某个上下文、随后 exit 掉watcher 回调会收到None而不是上下文对象见下一节注释。四、上下文监视器Python 3.14 新增文档中标注versionadded:: 3.14的一组新 API让 C 扩展能够感知上下文的切换int PyContext_AddWatcher(PyContext_WatchCallback callback); int PyContext_ClearWatcher(int watcher_id); /* 事件枚举 */ typedef enum { Py_CONTEXT_SWITCHED 1 } PyContextEvent; /* 回调函数类型 */ typedef int (*PyContext_WatchCallback)(PyContextEvent event, PyObject *obj);语义约定均来自文档并已在 Python/context.c 中验证PyContext_AddWatcher为当前解释器注册一个监视器回调返回一个非负 ID 句柄若 ID 耗尽则返回-1并设置异常。PyContext_ClearWatcher(watcher_id)注销该句柄成功返回0对从未注册的 ID 调用则返回-1并设异常ValueError。目前唯一的枚举事件是Py_CONTEXT_SWITCHED当前上下文切换到另一个上下文时触发回调收到的obj是现在当前的contextvars.Context对象若当前无上下文则为None。回调异常契约回调若返回-1且带异常该异常会通过PyErr_FormatUnraisable作为不可抛出异常打印回调进入时可能已经存在 pending 异常此时必须原样保留异常并返回0即回调内部不得调用任何可能设置异常的 API除非先保存并清除、返回前恢复异常状态。实现要点Python/context.c#L185-L230监视器存储在解释器状态中上限由 Include/internal/pycore_interp_structs.h#L20 的#define CONTEXT_MAX_WATCHERS 8决定——每个解释器最多注册 8 个监视器active_context_watchers位图用于 O(1) 地跳过未注册槽位。PyContext_AddWatcher线性扫描找第一个空槽位并置位PyContext_ClearWatcher对越界或空槽位分别抛ValueError: Invalid context watcher ID/No context watcher set for ID。context_switched()在每次 enter/exit 后递增线程状态上的context_ver计数器并广播事件——这个版本号同时是 3.2 节提到的PyContextVar缓存失效机制的基础。仓库中的 C API 测试示例位于 Modules/_testcapi/watchers.c第 702、721、773、782 行附近演示了注册、清理与 ID 耗尽场景可作为 C 侧用法参照Python 层行为测试位于 test/test_context_vars.py。一个典型的最小用法注册监视器并立即清理static int my_watcher(PyContextEvent event, PyObject *ctx) { if (event Py_CONTEXT_SWITCHED) { // ctx 是现在的 Context 对象或 None } return 0; } int id PyContext_AddWatcher(my_watcher); if (id 0) { /* 处理异常 */ } ... PyContext_ClearWatcher(id); /* 不再需要时注销 */五、ContextVar 变量操作函数5.1 创建变量PyContextVar_NewPyObject *PyContextVar_New(const char *name, PyObject *def);name仅用于内省与调试Python 层的var.name属性、repr输出def为该变量的默认值可为NULL表示无默认值出错返回NULL。实现Python/context.c#L305-L315先把 C 字符串转成str再交给contextvar_new。变量创建时会预计算哈希Py_HashPointer(addr) ^ PyObject_Hash(name)源码注释解释了动机——HAMT 树的结构编码在哈希中XOR 对象地址可以保证顺序分配的变量、同名的变量有不同的哈希避免哈希碰撞导致树退化变高Python/context.c#L913-L939。5.2 读取PyContextVar_Get 的完整返回值语义这是 C API 中最需要仔细对待的函数int PyContextVar_Get(PyObject *var, PyObject *default_value, PyObject **value);返回值查找过程中出错返回-1无论变量是否找到只要没出错都返回0*value的取值优先级文档原文已在实现中逐行确认变量在当前上下文中找到→ 指向找到的值未找到且default_value ! NULL→ 指向default_value未找到且变量本身有默认值var_default ! NULL→ 指向该默认值以上都不是 →NULL。引用语义除NULL外函数返回的是新引用调用方负责Py_DECREF。实现Python/context.c#L318-L382展示了完整的查找路径GIL 构建下先查var_cached缓存线程 ID 与context_ver都匹配才命中未命中则对当前上下文的 HAMT 做_PyHamt_Find并把结果写回缓存。注意末尾的not_found分支对def NULL与def ! NULL的分派完全对应上面 2/3/4 的优先级。Python 层ContextVar.get()就是薄封装C 侧*value NULL时抛LookupErrorPython/context.c#L1099-L1114。5.3 写入PyContextVar_SetPyObject *PyContextVar_Set(PyObject *var, PyObject *value);在当前上下文中把var设为value返回描述此次修改的新 Token 对象出错返回NULL。实现Python/context.c#L385-L416的顺序值得注意先通过_PyHamt_Find取出旧值找不到则NULL→ 用(ctx, var, old_val)构造 token → 再做_PyHamt_Assoc生成新 HAMT 树并原子替换ctx-ctx_vars。HAMT 的持久化特性保证旧值在旧树中仍可达写入不会改动任何已被其他上下文引用的数据。5.4 回滚PyContextVar_Resetint PyContextVar_Reset(PyObject *var, PyObject *token); /* 成功 0失败 -1 */把var恢复到PyContextVar_Set返回token之前的状态。从实现Python/context.c#L419-L454确认三道校验token 已被使用过 →RuntimeError: %R has already been used oncetoken 一次性语义token 不是由该var创建的 →ValueError: %R was created by a different ContextVar当前上下文不是 token 创建时所在的上下文 →ValueError: %R was created in a different Context。回滚动作若tok_oldval NULLset 前无值则从当前上下文的 HAMT 中删除该变量否则写回旧值。Python 层的with var.set(x):上下文管理器3.14 起在__exit__中正是调用PyContextVar_ResetPython/context.c#L1330-L1340。六、扩展模块中的典型用法将上述 API 组合起来一个完整的 C 侧使用范式如下要求扩展按标准 CPython 扩展方式构建不启用Py_LIMITED_API#define Py_LIMITED_API_CHECK 1 #include Python.h #include cpython/context.h /* 提供 PyContext_* / PyContextVar_* 声明 */ /* 在请求处理前保存当前上下文结束后恢复 */ static PyObject * process_in_copied_context(PyObject *self, PyObject *callable) { PyObject *ctx PyContext_CopyCurrent(); if (ctx NULL) { return NULL; } /* 在此可以 PyContext_Enter(ctx) 后执行同步 C 代码 其间 PyContextVar_Set/Get 均作用于该上下文 */ /* 典型用法读取一个请求级变量 */ PyObject *var PyContextVar_New(request_id, NULL); if (var NULL) { Py_DECREF(ctx); return NULL; } PyObject *value NULL; int rc PyContextVar_Get(var, NULL, value); if (rc 0) { Py_DECREF(var); Py_DECREF(ctx); return NULL; } /* value 若非 NULL 是新建引用用完 Py_DECREF(value) */ Py_XDECREF(value); Py_DECREF(var); Py_DECREF(ctx); return Py_NewRef(Py_None); }要点回顾PyContextVar_Get的三参签名中default_value传NULL表示未找到且无默认值时把*value置NULL这是 C 层区分无值与值恰好是None的唯一方式通过PyContext_NewPyContext_Enter/Exit可以在 C 侧显式管理上下文生命周期例如在 C 实现的调度器/线程池边界上做快照与恢复对asyncio场景Python 层的contextvars.copy_context()Context.run()Lib/contextvars.py与这里的 C API 是同一底层机制的两种暴露。七、API 速查表与文件索引API签名返回值约定PyContext_New(void)新引用 /NULLPyContext_Copy(PyObject *ctx)浅拷贝新引用 /NULLPyContext_CopyCurrent(void)当前线程上下文的浅拷贝 /NULLPyContext_Enter(PyObject *ctx)0/-1PyContext_Exit(PyObject *ctx)0/-1PyContext_AddWatcher3.14(PyContext_WatchCallback)监视器 ID /-1异常PyContext_ClearWatcher3.14(int watcher_id)0/-1异常PyContextVar_New(const char *name, PyObject *def)新引用 /NULLPyContextVar_Get(var, default_value, **value)0/-1*value新引用或NULLPyContextVar_Set(PyObject *var, PyObject *value)Token 新引用 /NULLPyContextVar_Reset(PyObject *var, PyObject *token)0/-1PyContext[_Var|_Token]_CheckExact(PyObject *o)真/假永不失败o不得为NULL关键文件索引API 文档主体Doc/c-api/contextvars.rst公共头文件Include/cpython/context.h核心实现Python/context.c模块注册copy_context与类型导出Python/_contextvars.c内部结构定义Include/internal/pycore_context.h监视器上限CONTEXT_MAX_WATCHERS 8Include/internal/pycore_interp_structs.hC 侧测试示例Modules/_testcapi/watchers.cPython 层模块与文档Lib/contextvars.py、Doc/library/contextvars.rst行为测试test/test_context_vars.py【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表