PyInstaller打包多线程Python应用时出现'cannot pickle '_struct.Struct' object'错误求助
PyInstaller打包多线程Python应用时出现'cannot pickle '_struct.Struct' object'错误求助
问题原因分析
首先帮你明确错误的核心:
- Pickle序列化失败:在Windows环境下,你的
ConnectionChannel使用了multiprocessing.Manager().dict()作为跨进程共享的连接存储。这个共享字典要求存入的所有对象必须是可被pickle序列化的,但你的Device对象里的the_connection(来自DeviceCommunicator.connect)包含了底层C扩展实现的_struct.Struct对象——这类底层通信连接对象几乎都是不可序列化的,所以当你执行self.connections.update({device.id: device})时,尝试序列化整个Device对象就会触发报错。 - 隐藏的设计问题:即使能解决序列化问题,跨进程共享底层设备连接本身就是不安全的。绝大多数硬件/网络通信库的连接对象都不是进程安全的,多个进程同时操作同一个连接会导致数据混乱、连接崩溃等问题。
解决方案(按改动量从小到大排序)
方案1:最简单的改动——改用单进程运行Uvicorn
你的应用是桌面端,并发量通常不高,完全可以把Uvicorn的多worker模式改成单进程,这样就不需要跨进程共享连接,自然也不会有序列化的问题。
修改server.py里的run_server函数:
def run_server(desktop=False, port=8123): global app create_production_app(desktop) if desktop: path_to_frontend = path.join(bundle_dir, "frontend") app.mount( "/", StaticFiles(directory=path_to_frontend, html=True), name="static" ) # 注释或删除Windows下的Manager字典设置 # if platform.system() == "Windows": # connection_channel.set_multithreading_manager(Manager().dict()) server = uvicorn.Server( config=uvicorn.Config( "server:app", port=port, host=HOST, workers=1, # 把workers从多进程改成1(单进程) interface=INTERFACE ) ) server.run()
同时,ConnectionChannel可以保持原样,因为现在connections是单进程内的普通字典,不需要任何序列化操作。
方案2:保留多worker——拆分序列化与连接对象
如果确实需要多进程提升性能,可以修改ConnectionChannel的逻辑:跨进程共享字典只存可序列化的设备元数据,而把不可序列化的连接对象存在每个进程本地的存储中,避免跨进程传递连接对象。
修改ConnectionChannel.py:
import multiprocessing import logging from Device.DeviceCore import Device # 注意导入路径 class ConnectionChannel: def __init__( self, id: int = 0, name: str = "default" ): self.id = id self.name = name self.max_connections = -1 self.connections = {} # 跨进程共享:只存设备元数据 self.local_connections = multiprocessing.local() # 进程本地存储:存连接对象 def set_multithreading_manager(self, manager): self.connections = manager def add_connection(self, device): if self.max_connections != -1 and len(self.connections) > self.max_connections: return try: did_connect = device.connect() if did_connect == True: # 共享字典只存可序列化的元数据 self.connections[device.id] = { "id": device.id, "connection_url": device.connection_url, "name": device.name } # 连接对象存在当前进程的本地存储 if not hasattr(self.local_connections, "devices"): self.local_connections.devices = {} self.local_connections.devices[device.id] = device return f"{device.name} Connected" else: raise Exception("Device Did not Connect") except Exception as e: logging.info(f"Error in Connection Channel! {str(e)}") raise e def get_device(self, id: int): # 优先从当前进程本地取连接对象 if hasattr(self.local_connections, "devices") and id in self.local_connections.devices: return self.local_connections.devices[id] # 本地没有的话,从共享元数据重建连接 device_meta = self.connections.get(id) if not device_meta: return None # 重新创建Device对象并连接 device = Device( id=device_meta["id"], connection_url=device_meta["connection_url"], name=device_meta["name"] ) device.connect() # 存入本地存储 if not hasattr(self.local_connections, "devices"): self.local_connections.devices = {} self.local_connections.devices[id] = device return device
这个方案既解决了序列化问题,又避免了多进程共享连接的安全风险,每个进程维护自己的设备连接。
关于你尝试的__getstate__/__setstate__为什么没生效
这个思路本身是对的,但问题在于:每次从共享字典取出Device对象时,都会触发反序列化并执行__setstate__,这意味着每次获取设备都要重新连接,会导致重复连接、设备资源浪费或操作延迟。如果一定要用这个方法,可以修改Device类:
from DeviceCommunicator import connect class Device: def __init__( self, id: int = 0, connection_url: str = "tcp:localhost:8080", name: str = "Default", ) -> None: self.id = id self.connection_url = connection_url self.name = name self.the_connection = None def connect(self) -> bool: try: self.the_connection = connect(self.connection_url) return True except Exception as e: print(str(e)) return False def __getstate__(self): # 序列化时移除不可pickle的连接对象 state = self.__dict__.copy() del state['the_connection'] return state def __setstate__(self, state): # 反序列化时恢复属性并自动重连 self.__dict__.update(state) self.connect()
但我更推荐前两个方案,因为这个方案的重连逻辑会带来额外的开销和潜在问题。
总结
- 如果你的桌面应用并发需求低,优先选方案1,改动最小,快速解决问题;
- 如果需要利用多核提升性能,选方案2,既解决序列化问题,又修复了跨进程共享连接的潜在风险;
- 不推荐用
__getstate__/__setstate__的方案,除非你能接受每次操作设备都要重新连接的开销。
备注:内容来源于stack exchange,提问作者Anmar Hani
相关产品推荐
相关产品推荐

