PyInstaller打包使用tqdm的Python程序退出时异常重启问题
问题现象
开发的Python程序支持可选调用tqdm为长耗时计算场景展示进度条,使用PyInstaller完成打包后存在如下表现:
- 未启用tqdm功能时,打包后的程序可正常运行至流程结束,无异常
- 启用tqdm时,程序可正常展示进度条、运行至流程末尾,随后抛出警告信息,且打包后的程序会携带错误参数自动重启
抛出的警告内容如下:
multiprocessing/resource_tracker.py:104: UserWarning: resource_tracker: process died unexpectedly, relaunching. Some resources might leak.
程序调用tqdm的逻辑为遍历列表成员的普通循环,本身未主动导入multiprocessing模块,核心实现代码:
def best_cost(self, guesses, steps=1, verbose=0) -> None: """ find the guess with the minimum cost """ self.cost = float("inf") for guess in guesses if verbose < 3 else tqdm(guesses, mininterval=1.0): groups = dict() for (word, frequency) in self.words.items(): result = calc_result(guess, word) groups.setdefault(result, {})[word] = frequency self.check_cost(guess, groups, steps=steps) return
复现环境
- 硬件系统:搭载Intel处理器的macOS Monterey V12.4
- Python版本:3.10.4(Python 3.9版本下问题同样存在)
- 运行环境:virtualenv虚拟环境,pip安装依赖版本为pyinstaller 5.1、pyinstaller-hooks-contrib 2022.7、tqdm 4.64.0
- 复现规律:PyInstaller无论使用
--onedir还是--onefile参数打包,问题均稳定复现;问题仅出现在打包后的程序中,IDE内直接运行源码无异常,无法通过VSCode等工具直接调试排查。
根因分析
根据multiprocessing模块官方文档说明:Unix系统下使用spawn或forkserver启动方式时,会额外启动resource_tracker进程,负责跟踪程序进程创建的未链接命名系统资源(如命名信号量semaphores、SharedMemory对象);所有进程退出后resource_tracker会清理剩余被追踪对象,正常场景下无剩余资源,若进程被信号杀死则可能出现资源泄漏——泄漏的信号量、共享内存段在系统重启前不会自动清理,既可能触发系统命名信号量数量限制,也会占用物理内存空间。
该问题触发逻辑如下:
- 业务代码本身未主动导入、使用multiprocessing模块
- tqdm内部会自动导入multiprocessing模块,为进度条实例创建基于信号量的锁,且未提供手动删除信号量资源的公开方法,按设计该类资源应在程序结束时自动释放
- PyInstaller打包后的程序运行时本身也会调用multiprocessing模块,打包后的运行环境会干扰tqdm所持信号量资源的正常释放顺序,导致resource_tracker误判主进程异常退出,抛出警告并触发程序自动重启。
可行解决方案
- 强制tqdm禁用multiprocessing锁:初始化tqdm时传入
lock_args=False参数,直接关闭tqdm内部基于multiprocessing创建的信号量锁,单进程场景下不会对进度条展示、功能逻辑产生任何影响,可从根源避免resource_tracker追踪tqdm创建的信号量资源,修改示例如下:
for guess in guesses if verbose < 3 else tqdm(guesses, mininterval=1.0, lock_args=False):
- 入口处添加multiprocessing打包兼容配置:在代码最外层
if __name__ == '__main__':块的首行添加PyInstaller冻结支持配置,同时强制multiprocessing使用fork启动模式(macOS下Python 3.8+默认启动模式为spawn,是触发resource_tracker独立进程启动的直接原因),配置代码如下:
import multiprocessing if __name__ == '__main__': multiprocessing.freeze_support() multiprocessing.set_start_method('fork', force=True) # 原有业务主逻辑从这里开始
注意:使用该方案前需确认程序所有依赖库兼容fork启动模式,若存在线程、锁相关的兼容异常,优先选择第一种方案。
- 升级依赖到修复版本:将pyinstaller升级至5.3及以上版本、tqdm升级至4.66.0及以上版本,高版本依赖已经修复了部分打包场景下multiprocessing资源清理顺序错乱的问题,升级后无需修改业务代码即可解决部分场景下的异常重启问题。
内容的提问来源于stack exchange,提问作者jzzz
相关产品推荐
相关产品推荐

