如何将多个标签放入不同进程中

1 投票
2 回答
526 浏览
提问于 2025-04-17 15:17

我在解决一个问题已经有一段时间了(我是一名化学工程师,所以我理解代码的速度比较慢),这个问题是如何让多个标签页在各自的进程中运行,并且每个标签页都有自己的数据在matplotlib图表中显示。 我遇到了很多“序列化错误”,想知道有没有什么简单的解决办法。我认为这些序列化错误的主要原因是我试图将一个对象作为属性传递给标签页对象。这个对象不仅包含一些数据,还有很多其他对象,帮助处理它所包含的数据。 我觉得这些对象非常好用,也很必要,但我也意识到它们导致了序列化的问题。 以下是我代码的一个非常简化的版本: (如果你想复制粘贴测试,这段代码仍然可以运行。)

import multiprocessing as mp
from PyQt4 import QtGui, QtCore
import numpy as np
import matplotlib
matplotlib.use('QtAgg')
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib import figure
import sys
import lmfit

# This object will just hold certain objects which will help create data objects ato be shown in matplotlib plots
# this could be a type of species with properties that could be quantized to a location on an axis (like number of teeth)
#, which special_object would hold another quantization of that property (like length of teeth) 
class object_within_special_object:
    def __init__(self, n, m):
        self.n = n
        self.m = m
    def location(self, i):
        location = i*self.m/self.n
        return location
    def NM(self):
        return str(self.n) + str(self.m)
# This is what will hold a number of species and all of their properties, 
# as well as some data to try and fit using the species and their properties
class special_object:
    def __init__(self, name, X, Y):
        self.name = name
        self.X = X
        self.Y = Y
        self.params = lmfit.Parameters()
        self.things = self.make_a_whole_bunch_of_things()
        for thing in self.things:
            self.params.add('something' + str(thing.NM()) + 's', value = 3)
    def make_a_whole_bunch_of_things(self):
        things = []
        for n in range(0,20):
            m=1
            things.append(object_within_special_object(n,m))
        return things
# a special type of tab which holds a (or a couple of) matplotlib plots and a special_object ( which holds the data to display in those plots)
class Special_Tab(QtGui.QTabWidget):
    def __init__(self, parent, special_object):
        QtGui.QTabWidget.__init__(self, parent)
        self.special_object = special_object
        self.grid = QtGui.QGridLayout(self)
        # matplotlib figure put into tab
        self.fig = figure.Figure()
        self.plot = self.fig.add_subplot(111)
        self.line, = self.plot.plot(self.special_object.X, self.special_object.Y, 'r-')
        self.canvas = FigureCanvas(self.fig)
        self.grid.addWidget(self.canvas)
        self.canvas.show()
        self.canvas.draw()
        self.canvas_BBox = self.plot.figure.canvas.copy_from_bbox(self.plot.bbox)
        ax1 = self.plot.figure.axes[0]
    def process_on_special_object(self):
        # do a long fitting process involving the properties of the special_object
        return
    def update_GUI(self):
        # change the GUI to reflect changes made to special_object
        self.line.set_data(special_object.X, special_object.Y)
        self.plot.draw_artist(self.line)
        self.plot.figure.canvas.blit(self.plot.bbox)
        return
# This window just has a button to make all of the tabs in separate processes
class MainWindow(QtGui.QMainWindow):
    def __init__(self, parent = None):
        # This GUI stuff shouldn't be too important
        QtGui.QMainWindow.__init__(self)
        self.resize(int(app.desktop().screenGeometry().width()*.6), int(app.desktop().screenGeometry().height()*.6))
        self.tabs_list = []
        central_widget = QtGui.QWidget(self)
        self.main_tab_widget = QtGui.QTabWidget()
        self.layout = QtGui.QHBoxLayout(central_widget)
        button = QtGui.QPushButton('Open Tabs')
        self.layout.addWidget(button)
        self.layout.addWidget(self.main_tab_widget)
        QtCore.QObject.connect(button, QtCore.SIGNAL("clicked()"), self.open_tabs)
        self.setCentralWidget(central_widget)
        central_widget.setLayout(self.layout)

    # Here we open several tabs and put them in different processes
    def open_tabs(self):
        for i in range(0, 10):
            # this is just some random data for the objects
            X = np.arange(1240.0/1350.0, 1240./200., 0.01)
            Y = np.array(np.e**.2*X + np.sin(10*X)+np.cos(4*X))
            # Here the special tab is created
            new_tab = Special_Tab(self.main_tab_widget, special_object(str(i), X, Y))
            self.main_tab_widget.addTab(new_tab, str(i))
            # this part works fine without the .start() function
            self.tabs_list.append(mp.Process(target=new_tab))
            # this is where pickling errors occur
            self.tabs_list[-1].start()
        return


if __name__ == "__main__":
    app = QtGui.QApplication([])
    win = MainWindow()
    win.show()
    sys.exit(app.exec_())

我注意到错误来自于matplotlib的坐标轴(我不太确定为什么),错误信息是pickle.PicklingError: Can't pickle <class 'matplotlib.axes.AxesSubplot'>: it's not found as matplotlib.axes.AxesSubplot。此外,我还发现如果注释掉matplotlib的图表,也会出现序列化错误pickle.PicklingError: Can't pickle <function <lambda> at 0x012A2B30>: it's not found as lmfit.parameter.<lambda>。我认为这是因为lambda函数不能被序列化,而我猜lmfit的某个地方有一个lambda函数……但我真的不知道该怎么做才能绕过这些错误。

奇怪的是,我在原始代码中看到的错误(不是这里展示的简化版本)稍微不同,但基本上意思是一样的。我的其他代码中出现的错误是pickle.PicklingError: Can't pickle 'BufferRegion' object: <BufferRegion object at 0x037EBA04>

有没有人能提供更好的解决方案,比如调整对象的传递方式或者其他想法?

非常感谢你们的时间和努力,任何关于这个问题的帮助我都很感激。

编辑:我尝试了unutbu的想法,做了一些修改,调整了进程函数的位置。 提议的解决方案唯一的问题是do_long_fitting_process()函数调用了另一个函数,这个函数会迭代更新matplotlib图表中的线条。所以do_long_fitting_process()需要访问Special_Tab的属性,以便更改它们并在GUI中显示更新。
我尝试通过将do_long_fitting_process()函数放到全局函数中来实现这一点,并调用这个:

[code] def open_tabs(self): for i in range(0, 10): ... self.tabs_list.append(new_tab

        consumer, producer = mp.Pipe()
        process = mp.Process(target=process_on_special_object, args=(producer,))
        process.start()
        while(True):
            message = consumer.recv()
            if message == 'done':
                break
            tab.update_GUI(message[0], message[1])
        process_list[-1].join()

[/code]

在这里,我通过mp.Pipe()将数据传递给update_GUI(),但一旦我开始进程,窗口就会变成“未响应”。

2 个回答

1

问题在于,并不是所有的Axes对象的部分都可以被序列化,而序列化是为了在不同的进程之间传递数据。我的建议是稍微调整一下你的代码,把计算的部分放到单独的进程中去处理(也就是说,任何需要花费超过一小段时间的操作),而把所有的绘图工作留在主进程中,只在进程之间传递数据。

另外一个选择是使用QThread。你可以查看这个链接 time.sleep() required to keep QThread responsive?,里面有两种实现方式(一个在提问中,另一个在我的回答里)。

1

把图形界面(GUI)的代码和计算的代码分开。图形界面必须在一个单独的进程中运行(虽然它可以启动多个线程)。把计算的代码放在 special_object 里面。

当你想要进行一个耗时的计算时,让 Special_Tab 调用 mp.Process

class special_object:
    def do_long_fitting_process(self):
        pass    

class Special_Tab(QtGui.QTabWidget):    
    def process_on_special_object(self):
        # do a long fitting process involving the properties of the
        # special_object
        proc = mp.Process(target = self.special_object.do_long_fitting_process)
        proc.start()

class MainWindow(QtGui.QMainWindow):
    def open_tabs(self):
        for i in range(0, 10):
            ...
            self.tabs_list.append(new_tab)
            new_tab.process_on_special_object()

撰写回答