pybind11循环调用含TensorFlow依赖的Python函数出现内存泄漏及崩溃
核心问题根因
- 内存单调上涨的原因:
- Python解释器的垃圾回收为分代延迟触发机制,不会在单次函数调用结束后立刻回收运行期间产生的临时对象
- TensorFlow、Numpy这类依赖原生内存池的第三方库,默认会长期持有已分配的内存,不会主动将闲置内存归还给操作系统
- 原代码每次循环调用都重复通过
py::module::import获取模块、函数对象的逻辑,会持续产生临时的pybind11对象引用,叠加GC延迟进一步推高内存占用
- 反复初始化/销毁解释器导致崩溃的原因:
- 所有引入的C扩展模块(TensorFlow、Numpy、redis-py等)在加载时,会将全局状态、对象缓存注册到Python解释器的全局表中,一旦调用
py::finalize_interpreter()销毁解释器,这些注册的指针会直接变成野指针 - 下一次重新初始化解释器时,重新加载这些C扩展模块会访问已释放的野指针、重复注册全局状态,直接触发段错误,这是pybind11当前对单进程内多次初始化/销毁带C扩展的Python环境的本质限制,没有可靠的兼容方案
- 所有引入的C扩展模块(TensorFlow、Numpy、redis-py等)在加载时,会将全局状态、对象缓存注册到Python解释器的全局表中,一旦调用
可落地优化方案
- 从根源规避崩溃:整个进程生命周期只初始化一次Python解释器,解释器的销毁逻辑放在进程完全退出的最后阶段执行,绝对不要在循环调用的业务流程里反复初始化、销毁解释器
- 修正代码本身的语法错误:原代码中的
del mcobj1;是Python语法,C中动态分配的堆对象需要使用delete mcobj1;释放,避免C侧本身的内存泄漏 - 优化模块/函数引用的获取逻辑:
- 在
myclass构造函数完成解释器初始化、sys.path配置之后,提前将mydl模块对象、myfunc函数对象存储为类的成员变量,循环业务调用时直接复用预存的函数引用,避免每次循环重复查询模块缓存、生成临时py::object带来的额外开销
- 在
- 主动触发内存回收,解决内存持续上涨问题:
- 每次
myfunc执行完成后,在C++侧主动调用Python的垃圾回收接口:py::module::import("gc").attr("collect")(),强制触发一次全量分代GC,释放函数调用期间生成的临时Python对象(Numpy数组、临时Redis连接、中间计算结果等) - 调整TensorFlow运行时配置,关闭无限制的内存预申请:在
mydl.py文件的最开头加入如下配置,让TensorFlow按需申请内存,不长期预占整块内存、可将闲置内存归还给操作系统:import tensorflow as tf # GPU侧按需申请内存,不一次性预占全部显存 for gpu in tf.config.list_physical_devices("GPU"): tf.config.experimental.set_memory_growth(gpu, True) # 限制CPU侧线程数,避免默认线程池占用过多内存 tf.config.threading.set_intra_op_parallelism_threads(2) tf.config.threading.set_inter_op_parallelism_threads(2) - 优化Redis客户端逻辑:不要每次函数调用都新建Redis连接对象,在Python模块内实现连接复用,避免临时连接的缓冲区、客户端对象长期被垃圾回收机制挂起无法释放
- 每次
- 做好pybind11侧的引用计数管理:所有临时的
py::object对象都放在最小的作用域内完成类型转换、使用,出作用域后及时析构释放引用,避免临时对象的引用被长期挂在全局作用域中
注意:网上流传的“每次业务调用都初始化/销毁解释器”的写法仅适用于纯Python代码的极简场景,只要业务逻辑中加载了带原生C扩展的第三方库,这种写法必然触发段错误,没有稳定的修复路径。
内容的提问来源于stack exchange,提问作者May A
相关产品推荐
相关产品推荐

