You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyInstaller打包多线程Python应用时出现'cannot pickle '_struct.Struct' object'错误求助

PyInstaller打包多线程Python应用时出现'cannot pickle '_struct.Struct' object'错误求助

问题原因分析

首先帮你明确错误的核心:

  1. Pickle序列化失败:在Windows环境下,你的ConnectionChannel使用了multiprocessing.Manager().dict()作为跨进程共享的连接存储。这个共享字典要求存入的所有对象必须是可被pickle序列化的,但你的Device对象里的the_connection(来自DeviceCommunicator.connect)包含了底层C扩展实现的_struct.Struct对象——这类底层通信连接对象几乎都是不可序列化的,所以当你执行self.connections.update({device.id: device})时,尝试序列化整个Device对象就会触发报错。
  2. 隐藏的设计问题:即使能解决序列化问题,跨进程共享底层设备连接本身就是不安全的。绝大多数硬件/网络通信库的连接对象都不是进程安全的,多个进程同时操作同一个连接会导致数据混乱、连接崩溃等问题。

解决方案(按改动量从小到大排序)

方案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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 13:53:09