
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),僅供參考