尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

pybind11 高级杂项完全指南:GIL 管理、自由线程/子解释器支持与多模块协作

pybind11 高级杂项完全指南:GIL 管理、自由线程/子解释器支持与多模块协作 pybind11 高级杂项完全指南GIL 管理、自由线程/子解释器支持与多模块协作【免费下载链接】pybind11Seamless operability between C11 and Python项目地址: https://gitcode.com/GitHub_Trending/py/pybind11本文是 pybind11 官方文档《Miscellaneous》docs/advanced/misc.rst的系统性展开覆盖 C/Python 绑定开发中最易踩坑的几个高级话题预处理宏的逗号陷阱、全局解释器锁GIL的获取与释放、Python 3.13 自由线程free-threading与 3.12 隔离子解释器sub-interpreter支持、多扩展模块间的类型共享以及基于 Sphinx 的文档生成与 docstring 控制。读完本文你将掌握在真实模块中正确处理并发、跨模块继承、模块退出清理和文档产出的完整实战方案并能结合仓库源码理解其底层实现原理。一、便捷宏的使用注意事项pybind11 提供了一批便捷宏典型代表是PYBIND11_DECLARE_HOLDER_TYPE与PYBIND11_OVERRIDE_*系列。由于它们只是交给 C 预处理器求值的宏预处理器没有类型概念当模板实参里出现逗号时宏会被错误地拆分。例如PYBIND11_OVERRIDE(MyReturnTypeT1, T2, ClassT3, T4, func)C 预处理器会把上面的调用解释成5 个参数每个逗号后都开始一个新参数而不是 3 个。解决方式有两种使用类型别名或用PYBIND11_TYPE宏把含逗号的类型整体包起来// 版本 1使用类型别名 using ReturnType MyReturnTypeT1, T2; using ClassType ClassT3, T4; PYBIND11_OVERRIDE(ReturnType, ClassType, func); // 版本 2使用 PYBIND11_TYPE 宏 PYBIND11_OVERRIDE(PYBIND11_TYPE(MyReturnTypeT1, T2), PYBIND11_TYPE(ClassT3, T4), func)PYBIND11_TYPE的实现非常简单就是#define PYBIND11_TYPE(...) __VA_ARGS__见 include/pybind11/cast.h#L2447-L2450它借助可变参数宏...把整个逗号序列作为一个参数原样透传。需要特别说明的是PYBIND11_MAKE_OPAQUE不需要上述任何规避手段。因为它内部同样使用#define PYBIND11_MAKE_OPAQUE(...)的可变参数形式展开见 include/pybind11/cast.h#L2439-L2445逗号不会造成参数计数错误。PYBIND11_OVERRIDE、PYBIND11_OVERRIDE_PURE、PYBIND11_OVERRIDE_NAME、PYBIND11_OVERRIDE_PURE_NAME在 include/pybind11/pybind11.h#L3953-L4004 中定义它们内部对ret_type和cname都套用了PYBIND11_TYPE所以你传ClassT3, T4这种带逗号的模板类型时其实已经在内部被包裹原文档示例之所以要显式包裹主要是为了在旧代码/可读性场景下保持一致的写法。二、全局解释器锁GIL与 RAII 管理2.1 GIL 的基本规则Python C API 规定当前线程必须持有 GIL 才能安全访问 Python 对象。因此当 Python 通过 pybind11 调用 C 时GIL 必然处于持有状态且 pybind11 永远不会隐式释放它void my_function() { /* 从 Python 调用本函数时 GIL 已被持有 */ } PYBIND11_MODULE(example, m) { m.def(my_function, my_function); }pybind11 在确定自己要调用 Python 代码时会主动确保 GIL 被持有。两种典型场景通过std::function把 Python 回调传给 CC 侧调用该回调时内置包装器会先获取 GIL 再调用 Python 回调PYBIND11_OVERRIDE系列宏在回调 Python 前也会获取 GIL——其展开实现PYBIND11_OVERRIDE_IMPL的第一步就是pybind11::gil_scoped_acquire gil;见 include/pybind11/pybind11.h#L3914-L3917。反之如果 C 代码由其他 C 代码调用却要访问 Python 状态则必须显式获取/释放 GIL。其中有一个特别隐蔽的死锁场景C 块作用域静态变量的初始化器会回调 Python这与静态变量初始化守卫互斥锁相互作用详见 docs/advanced/deadlock.md。2.2 gil_scoped_release 与 gil_scoped_acquirepy::gil_scoped_release和py::gil_scoped_acquire两个 RAII 类可在 C 函数体内安全地释放/重新获取 GIL定义见 include/pybind11/gil.h。这样长时间运行的 C 代码就能借助多个 Python 线程实现并行但必须极度谨慎只要 C 代码存在任何访问 Python 对象的可能就应该用gil_scoped_acquire重新获取 GIL。以文档《重写虚函数》overriding virtuals中的Animal为例正确写法如下关键改动以注释标注class PyAnimal : public Animal, public py::trampoline_self_life_support { public: /* 继承构造函数 */ using Animal::Animal; /* Trampoline每个虚函数需要一个 */ std::string go(int n_times) { /* PYBIND11_OVERRIDE_PURE 会在访问 Python 状态前自动获取 GIL */ PYBIND11_OVERRIDE_PURE( std::string, /* 返回类型 */ Animal, /* 父类 */ go, /* 函数名 */ n_times /* 参数 */ ); } }; PYBIND11_MODULE(example, m) { py::class_Animal, PyAnimal, py::smart_holder animal(m, Animal); animal .def(py::init()) .def(go, Animal::go); py::class_Dog, py::smart_holder(m, Dog, animal) .def(py::init()); m.def(call_go, [](Animal *animal) - std::string { // 从 Python 调用时 GIL 已被持有在调用可能长时间运行的 C 代码前先释放 py::gil_scoped_release release; return call_go(animal); }); }上面这段call_go包装器还可以用call_guard策略简化效果完全相同m.def(call_go, call_go, py::call_guardpy::gil_scoped_release());2.3 常见 GIL 错误的排查清单未能正确持有 GIL 是 pybind11 代码中最常见的 bug 来源之一。遇到 GIL 相关错误时建议按以下清单逐项排查全局变量的构造函数/析构函数中是否出现 pybind11 对象或调用了 pybind11 函数全局静态上下文中通常不允许调用任何 Python 函数推荐改用惰性初始化lazy initialization并在程序结束时主动泄漏。其他 C 结构体中是否含有 pybind11 对象成员一个常被忽略的规则是pybind11 对象的拷贝构造函数会增加引用计数因此调用任何含有 pybind11 成员的 C 类的拷贝构造函数时都必须持有 GIL。这在复杂程序中很难追踪务必三思。C 析构函数调用 Python 函数尤其危险析构函数可能因异常而在各种意外时刻被调用。C 块作用域静态变量初始化回调 Python可能导致死锁见 docs/advanced/deadlock.md。用 debug 构建运行代码pybind11 内置的断言会在部分 GIL 处理错误如引用计数操作时抛出异常有助于尽早发现问题。三、自由线程Free-threading支持pybind11 支持 Python 3.13 的实验性自由线程构建free-threaded builds。pybind11 的内部数据结构是线程安全的。要让模块能在自由线程模式下使用只需在PYBIND11_MODULE的第三个参数传入py::mod_gil_not_used标签PYBIND11_MODULE(example, m, py::mod_gil_not_used()) { py::class_Animal animal(m, Animal); // 其他绑定…… }从源码看mod_gil_not_used是一个带有flag_的 tag 类见 include/pybind11/pybind11.h#L1461-L1473它对应 CPython 的Py_MOD_GIL_NOT_USED模块槽位同时 pybind11 也提供py::mod_gil_used()当前默认行为显式声明模块需要 GILinclude/pybind11/pybind11.h#L1475-L1480。需要强调三点加上该标签等同于承诺你的代码是线程安全的模块仍必须针对 Python 的自由线程分支free-threading branch构建才能真正启用自由线程加上该标签不会破坏与普通非自由线程Python 的兼容性。四、子解释器Sub-interpreter支持pybind11 支持 Python 3.12 稳定的隔离子解释器isolated sub-interpreters。pybind11 的内部数据结构对子解释器安全。要让模块能在隔离子解释器中被导入需在PYBIND11_MODULE的第三个或更靠后的参数传入py::multiple_interpreters::per_interpreter_gil()标签PYBIND11_MODULE(example, m, py::multiple_interpreters::per_interpreter_gil()) { py::class_Animal animal(m, Animal); // 其他绑定…… }multiple_interpreters类在 include/pybind11/pybind11.h#L1482-L1503 中定义内部是一个三值枚举level并最终映射到 CPython 的Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED/Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED/Py_MOD_PER_INTERPRETER_GIL_SUPPORTED模块槽位include/pybind11/pybind11.h#L1519-L1538。4.1 子解释器安全的最佳实践初始化函数会对每个导入该模块的解释器各执行一次绝不要在不同子解释器之间共享 Python 对象尽量避免全局/静态状态状态应保存在每个解释器内部例如绑定到 Python 对象的实例成员、globals()或解释器状态字典中C 代码中没有任何全局/静态状态的模块可能无需额外工作就天然子解释器安全避免在 C 变量中跨函数调用缓存 Python 对象——这是最容易引入子解释器 bug 的做法子解释器各自拥有独立的 GIL因此一个程序里可能存在多个相互独立的 GIL两个不同子解释器仍可能并发调用你的模块模块依然要考虑线程安全。4.2 其他子解释器标签pybind11 还支持共享单一全局 GIL 的传统legacy子解释器只需改用py::multiple_interpreters::shared_gil()标签启用纯 legacy 行为。若希望显式禁用子解释器支持使用py::multiple_interpreters::not_supported()标签——不指定任何 multiple_interpreters 标签时的默认行为就是 not_supported。延伸阅读docs/advanced/embedding.rst 的相应章节对嵌入场景下的子解释器与自由线程用法有进一步说明。五、并发与并行把模块同时做到子解释器安全 自由线程安全子解释器支持与自由线程支持互不蕴含自由线程安全的模块仍可持有全局/静态状态只要访问是线程安全的而子解释器安全的模块则不能同理子解释器安全的模块仍可依赖 GIL自由线程安全的模块则不能。以下面这个返回上一次计算结果的简单模块为例逐步演示如何把一段代码从仅支持普通 GIL 模式升级为同时满足自由线程与子解释器要求第 1 步基线版本既非自由线程安全也非子解释器安全PYBIND11_MODULE(example, m) { static size_t seed 0; m.def(calc_next, []() { auto old seed; seed (seed 1) * 10; return old; }); }seed没有任何同步保护多线程并发调用时行为不确定。第 2 步用原子操作实现自由线程安全PYBIND11_MODULE(example, m, py::mod_gil_not_used()) { static std::atomicsize_t seed(0); m.def(calc_next, []() { size_t old, next; do { old seed.load(); next (old 1) * 10; } while (!seed.compare_exchange_weak(old, next)); return old; }); }std::atomic加 compare-exchange 保证了即使多线程同时调用函数行为也完全一致。第 3 步把状态移入globals()实现子解释器安全但上面的全局/静态整数在子解释器间会互相干扰一个子解释器的调用会改变另一个看到的值因此需要把状态做成按解释器隔离。一种做法是把状态存在另一个 Python 对象上例如globals()PYBIND11_MODULE(example, m, py::multiple_interpreters::per_interpreter_gil()) { m.def(calc_next, []() { if (!py::globals().contains(myseed)) py::globals()[myseed] 0; size_t old py::globals()[myseed]; py::globals()[myseed] (old 1) * 10; return old; }); }这个模块对shared_gillegacy和per_interpreter_gil默认两种变体都是子解释器安全的多个子解释器可从不同线程并发调用同一函数因为每个子解释器的 GIL 保护着各自的 Python 对象。但计算过程没有同步模块又不再是自由线程安全的了。第 4 步用py::scoped_critical_section补齐自由线程安全#include pybind11/critical_section.h // ... PYBIND11_MODULE(example, m, py::multiple_interpreters::per_interpreter_gil(), py::mod_gil_not_used()) { m.def(calc_next, []() { size_t old; py::dict g py::globals(); py::scoped_critical_section guard(g); if (!g.contains(myseed)) g[myseed] 0; old g[myseed]; g[myseed] (old 1) * 10; return old; }); }至此该模块同时满足子解释器安全与自由线程安全。py::scoped_critical_section的实现在 include/pybind11/critical_section.h在非自由线程 Python 中它什么都不做构造/析构为空操作在自由线程 Python 中则通过 CPython 的PyCriticalSection_Begin/End可锁 1 或 2 个对象对对象加锁。警告关于临界区嵌套使用py::scoped_critical_section时不能嵌套且不要同时持有其他同步原语如std::mutex否则可能死锁。在 Python 3.13 中对已上锁对象再次加锁会先释放再重新获取因此不能用于读改写字典这类场景——字典在 CPython 内部也使用临界区如需这种能力3.13 上请改用std::mutex。Python 3.14 做了优化对已锁定对象加锁不再释放重锁从而修复了该问题。六、绑定序列数据类型、迭代器与切片协议要绑定一个完整的序列数据类型包括__len__长度查询、__iter__迭代器、切片协议等可以直接参考仓库中的完整示例tests/test_sequences_and_iterators.cpp 展示了如何绑定序列类型并实现各类常用操作对应的 Python 侧验证见 tests/test_sequences_and_iterators.py。这两份文件是实战中照抄即可的范本。七、把绑定代码拆分到多个扩展模块将绑定代码拆分到多个扩展模块、并互相引用对方声明的类型是 pybind11 直接支持的——一切照常工作无需特殊预防。唯一的例外是在另一个扩展模块中扩展继承其声明的类型。回顾《继承》一节的基础示例py::class_Pet pet(m, Pet); pet.def(py::initconst std::string ()) .def_readwrite(name, Pet::name); py::class_Dog(m, Dog, pet /* - 指定父类 */) .def(py::initconst std::string ()) .def(bark, Dog::bark);如果Pet的绑定定义在名为basic的模块中而Dog的绑定在别处那么pet变量就不存在了但py::class_Dog的构造函数仍需要它来表示继承关系。可以通过导入模块后取回类型对象解决py::object pet (py::object) py::module_::import(basic).attr(Pet); py::class_Dog(m, Dog, pet) .def(py::initconst std::string ()) .def(bark, Dog::bark);或者把基类作为py::class_的模板参数由 pybind11 自动查找对应的 Python 类型。与上面的代码一样这同样需要先执行一次import确保basic模块的绑定代码已经运行py::module_::import(basic); py::class_Dog, Pet(m, Dog) .def(py::initconst std::string ()) .def(bark, Dog::bark);两种方法在存在循环依赖时都会失败。7.1 符号可见性-fvisibilityhidden的影响pybind11 代码通常以隐藏符号的方式编译GCC/Clang 的-fvisibilityhidden这也是 pybind11 正常工作的前提之一但这会干扰跨扩展模块访问类型的能力。解决办法是手动导出被多个扩展模块使用的类型pybind11 为此提供了PYBIND11_EXPORT宏class PYBIND11_EXPORT Dog : public Animal { ... };PYBIND11_EXPORT在 include/pybind11/detail/common.h#L140-L145 中定义Windows 上展开为__declspec(dllexport)GCC/Clang 上展开为__attribute__((visibility(default)))。7.2 在扩展模块间共享任意 C 数据虽然很少用到但扩展模块之间也可以在运行时共享任意 C 对象。pybind11 内部库数据通过capsule 机制Python 的PyCapsule在模块间共享这套机制也可用来存取用户自定义数据。注意只有当扩展模块用相同版本的 pybind11 构建时才能看到其他扩展的数据。示例如下auto data reinterpret_castMyData *(py::get_shared_data(mydata)); if (!data) data static_castMyData *(py::set_shared_data(mydata, new MyData(42)));如果上述片段用于多个分别编译的扩展模块第一个被导入的模块会创建MyData实例并把mydata键关联到该指针之后导入的扩展模块就能通过同一个指针访问这份数据。底层实现见 include/pybind11/detail/internals.h#L1052-L1068共享数据存放在各模块共用的internals.shared_data映射中另外还提供get_or_create_shared_dataT的便捷模板可返回强类型引用。八、模块析构Module Destructorspybind11没有提供在模块销毁时执行清理代码的显式机制。在少数确需此功能的场景可以用Python capsule或带销毁回调的弱引用来模拟auto cleanup_callback []() { // 在此执行清理——本函数被调用时持有 GIL }; m.add_object(_cleanup, py::capsule(cleanup_callback));这种做法的潜在缺点是清理回调被调用时模块内暴露的类实例可能仍然存活能否接受通常取决于具体应用。也可以把 capsule 藏在某个类型对象内部从而确保在所有该类型实例被回收前不会调用它auto cleanup_callback []() { /* ... */ }; m.attr(BaseClass).attr(_cleanup) py::capsule(cleanup_callback);前两种方案都会在 Python 侧暴露一个危险的_cleanup属性从 API 角度可能不受欢迎因为 Python 里过早显式调用它会导致未定义行为。第三种方案用带清理回调的弱引用规避这个问题// 注册一个在 BaseClass 对象被回收时调用的回调 py::cpp_function cleanup_callback( [](py::handle weakref) { // 在此执行清理——本函数被调用时持有 GIL weakref.dec_ref(); // 释放弱引用 } ); // 创建带清理回调的弱引用并有意先泄漏它 (void) py::weakref(m.attr(BaseClass), cleanup_callback).release();注意PyPy 在解释器退出时不会回收对象。一个同样适用于 CPython 的替代方案是使用 Python 的atexit模块auto atexit py::module_::import(atexit); atexit.attr(register)(py::cpp_function([]() { // 在此执行清理——本函数被调用时持有 GIL }));九、使用 Sphinx 自动生成文档Sphinx 能够检查 pybind11 扩展模块的签名与 docstring自动生成多种格式的精美文档。使用时有两大注意点第一docstring 中不能包含 TAB 字符否则会破坏 docstring 解析例程。推荐使用 C11 原始字符串字面量raw string literal书写多行注释——Sphinx 会自动去除多余缩进但前提是所有行的缩进必须一致// 正确 m.def(foo, foo, Rmydelimiter( The foo function Parameters ---------- )mydelimiter); // 错误 m.def(foo, foo, Rmydelimiter(The foo function Parameters ---------- )mydelimiter);9.1 用py::options控制自动生成的签名与 docstring默认情况下pybind11 会为module_::def()和class_::def()注册的函数自动生成并前置一份签名。某些场景下你可能想提供自定义签名或完全去掉 docstring 以把函数排除在 Sphinx 文档之外。py::options类见 include/pybind11/options.h允许按需关闭自动签名PYBIND11_MODULE(example, m) { py::options options; options.disable_function_signatures(); m.def(add, [](int a, int b) { return a b; }, A function which adds two numbers); }py::options还提供另外两个开关均有对应的 enable 版本方法作用disable_function_signatures()关闭自动生成的函数签名默认开启disable_enum_members_docstring()关闭追加到枚举 docstring 末尾的枚举成员列表默认开启disable_user_defined_docstrings()关闭module_::def()、class_::def()与enum_()中用户自定义的 docstring但函数签名与枚举成员仍会进入 docstring除非另行关闭注意改动只影响options实例存活期间创建的函数绑定。当options在模块初始化函数末尾析构时设置会自动恢复为默认值避免产生副作用——这与 include/pybind11/options.h#L25-L26 中析构函数global_state() previous_state;的实现完全对应。9.2 避免 docstring 中出现 C 类型名docstring 是在声明时即调用.def(...)那一刻生成的此时参数与返回类型应当已被 pybind11 知晓。如果某个自定义类型尚未通过py::class_构造函数或自定义 type caster 暴露docstring 中的签名就会退化为 C 类型名| __init__(...) | __init__(self: example.Foo, arg0: ns::Bar) - None ^^^^^^^解决办法在类型被用作函数参数或返回类型之前先把对应的 C 类注册给 pybind11PYBIND11_MODULE(example, m) { auto pyFoo py::class_ns::Foo(m, Foo); auto pyBar py::class_ns::Bar(m, Bar); pyFoo.def(py::initconst ns::Bar()); pyBar.def(py::initconst ns::Foo()); }9.3 在 docstring 中设置内层类型提示当使用list、dict等 Python 泛型类型的 pybind11 包装器时docstring 只会显示泛型类型本身。可以用一组特殊的带类型版本泛型来传达内层类型PYBIND11_MODULE(example, m) { m.def(pass_list_of_str, [](py::typing::Listpy::str arg) { // arg 可以像 py::list 一样使用 )); }生成的 docstring 为pass_list_of_str(arg0: list[str]) - None。pybind11/typing.h中可用的特殊类型有py::TupleArgs...py::DictK, Vpy::ListVpy::SetVpy::CallableSignature警告与 Python 中的类型注解一样这些只是提示hints运行时与编译时都不会强制校验内容类型。十、结语与进一步阅读本文覆盖了 pybind11 进阶使用中最容易出问题的多个杂项主题。核心要点可归纳为宏参数中的模板逗号用PYBIND11_TYPE或类型别名解决GIL 的获取/释放遵循pybind11 不隐式释放、回调自动持有、纯 C 调用需显式管理三原则并善用gil_scoped_release/gil_scoped_acquire与call_guard自由线程用py::mod_gil_not_used()子解释器用py::multiple_interpreters::per_interpreter_gil()/shared_gil()/not_supported()两者互不蕴含需分别满足线程安全与解释器本地状态要求跨模块共享类型用module_::importpy::class_的 parent/template 参数必要时用PYBIND11_EXPORT导出符号数据共享用get_shared_data/set_shared_data模块清理可借 capsule、带回调弱引用或atexit实现文档生成善用py::options控制签名/枚举成员/自定义 docstring并用py::typing::*提供内层类型提示。如果你想继续深入推荐按顺序阅读仓库中的这些资源docs/advanced/deadlock.mdGIL 与静态变量初始化死锁、docs/advanced/functions.rstcall_guard 等调用策略、docs/advanced/classes.rst继承与 trampoline、docs/advanced/embedding.rst嵌入解释器并结合 tests/test_gil_scoped.cpp、tests/test_multiple_interpreters.py、tests/test_scoped_critical_section.cpp、tests/test_sequences_and_iterators.cpp 等测试用例进行实证验证。【免费下载链接】pybind11Seamless operability between C11 and Python项目地址: https://gitcode.com/GitHub_Trending/py/pybind11创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表