Python调用C++异步行为:GIL引发的同步问题及代码安全性验证
C++异步类Python调用的安全性分析
问题背景
我需要用C封装异步行为,通过pybind11/stl.h实现数据交互,不共享Python与C内存,仅传递C类的Python实例。此前在C绑定函数中运行异步任务无异常(C主函数与Python主函数结果一致),但以Python为主程序时,C异步线程出现同步问题。不确定该继续调试代码,还是GIL导致多线程下C++对象使用异常。
我认为Python与C++共用实例且多线程运行可能存在风险,目标实现:
- C++类包含三个方法:创建线程并保存句柄到类作用域;返回布尔值表示计算是否完成;返回缓存的计算结果
- Python端周期性调用检查方法,确认计算完成后获取结果
示例代码
C++代码(供Python调用)
#include <vector> #include <thread> #include <atomic> #include <iostream> class DummyClass { private: std::vector<std::vector<double>> _data; std::atomic<bool> _isDone{false}; const size_t _target = 100; std::vector<std::thread> _workers; public: DummyClass() = default; ~DummyClass() { for (auto& t : _workers) t.join(); _workers.clear(); } void do_dummy_async_task() { auto dummy_task = [&]() { using namespace std::chrono_literals; std::cout << "Dummy task started" << std::endl; // 模拟耗时操作,修改类成员_data for (int i = 0; i < _target; i++) { std::this_thread::sleep_for(5ms); _data.push_back(std::vector<double>(1000, 0)); } std::cout << "Dummy Task ended" << std::endl; _isDone = true; }; _workers.emplace_back(dummy_task); return; } bool isDone() { return _isDone; } std::vector<std::vector<double>> get_results(){ return _data; } size_t get_number_of_targets() { return _target; } };
Python主程序
from pyDummy import DummyClass import time def run_main(): obj = DummyClass() expecting = obj.get_number_of_results() # 注意:C++方法名为get_number_of_targets,此处名称不匹配 obj.do_async_task() # 注意:C++方法名为do_dummy_async_task,此处名称不匹配 print("Doing python things while Compiled Code does something else") while(not obj.is_Done()): # 注意:C++方法名为isDone,此处名称不匹配 time.sleep(1) results = obj.get_results() print(f"Done:\nExpected {expecting} and got {len(results)}!") if __name__ == "__main__": run_main()
代码安全性分析
核心结论:现有代码存在线程安全风险,并非GIL直接导致,但需修正同步逻辑与方法名匹配问题
GIL的影响
C线程运行时不会持有GIL(pybind11仅在Python调用C函数时临时获取GIL,C自主创建的线程默认不持有),因此GIL不会干扰C线程对类成员的操作。Python调用C的isDone()、get_results()等方法时,会短暂获取GIL,但这些方法本身是读操作或返回拷贝,不会阻塞C线程。线程安全隐患
_isDone使用std::atomic<bool>,读写操作原子化,无线程安全问题。_data在C++异步线程中执行push_back,而Python调用get_results()时会拷贝整个vector。std::vector::push_back在扩容时会导致内存重新分配,若此时Python端正在拷贝_data,会触发未定义行为(如拷贝到失效的内存区域)。此为核心风险点。- 析构函数中调用
join()等待所有线程完成,这部分逻辑安全,避免了对象销毁时线程仍在运行的问题。
- 代码中的命名错误
Python代码中调用的方法名与C类的方法名不匹配(如get_number_of_results()对应C的get_number_of_targets()),会直接导致运行时错误,需修正为与pybind11绑定后的名称一致。
修正建议
- 添加互斥锁保护共享数据
给_data添加std::mutex,在修改和读取时加锁:
// DummyClass私有成员添加 std::mutex _data_mutex; // 异步任务中修改_data时加锁 std::lock_guard<std::mutex> lock(_data_mutex); _data.push_back(std::vector<double>(1000, 0)); // get_results中拷贝数据时加锁 std::lock_guard<std::mutex> lock(_data_mutex); return _data;
- 修正Python端方法名
确保Python调用的方法名与C++绑定的名称一致,比如:
expecting = obj.get_number_of_targets() obj.do_dummy_async_task() while(not obj.isDone()):
- 可选:结果完整性校验
在get_results()中可检查_isDone状态,避免返回未完成的结果:
std::vector<std::vector<double>> get_results(){ if (!_isDone) { return {}; // 或抛出异常 } std::lock_guard<std::mutex> lock(_data_mutex); return _data; }
内容的提问来源于stack exchange,提问作者Riccardo Agazzotti
相关产品推荐
相关产品推荐

