PySide:多线程信号连接将插槽连接到错误的信号

PySide: Multithread signal connection connects slot to the wrong signal

我有一个 Python3 使用 PySide 的多线程应用程序。

Python版本:3.4

PySide 版本:1.2.4

我设置了一个有多个信号的信号器线程。我在不同的线程中也有一个接收器对象 运行(使用 QObject 和 moveToThread 确保所有代码实际运行在已建立的线程中)。

在接收器中,我在信号器线程中建立了多个与信号的连接。但是当我启动时,只有我建立的最后一个连接是正确连接的。其他连接最终导致错误的信号时隙相关性。

我尝试过不同的方法。一个例子是下面的代码。我也尝试过使用 queue.Queue 将连接请求从接收器发送到信号器,以便连接实际上是在信号器线程中建立的,但这看起来好像没有任何区别。

我的问题分为两部分:

  1. 在连接来自不同线程的多个信号槽时,如何确保正确的信号槽配对?
  2. 到底为什么会这样?

代码:

#!/usr/bin/env python3

from PySide import QtCore
import signal # just so that we can do ctrl-c
import time

class Information:
    def __init__(self, key, sig):
        self.key = str(key)
        self.sig = str(sig)

    def __str__(self):
        return 'Key {} for Signal {}'.format(self.key, self.sig)

class Signaller(QtCore.QThread):
    sig_dict = {0: None,
                1: None,
                2: None,
                3: None,
                4: None,
                5: None,
                6: None,
                7: None,
                8: None,
                9: None}

    def __init__(self, parent=None):
        super().__init__(parent)
        # Copy the discrete signals to the sig_dict dictionary
        for k in list(self.sig_dict.keys()):
            self.sig_dict[k] = getattr(self, 'signal_{}'.format(k))

    def run(self):
        for key, sig in self.sig_dict.items():
            print("Emitting Key {}: Signal {}".format(key, sig))
            sig.emit(Information(key, sig))

        self.exec_()

# We need to explicitly set attributes in the Signaller class
# to represent the signals because the metaclass only checks
# direct attributes when binding signals
for k in Signaller.sig_dict:
    setattr(Signaller, 'signal_{}'.format(k), QtCore.Signal(Information))

class Receiver(QtCore.QObject):
    start_signal = QtCore.Signal()

    def __init__(self, signaller, parent=None):
        super().__init__(parent)
        self.start_signal.connect(self.StartReceiving)
        self.signaller = signaller

    def StartReceiving(self):
        print("Connecting key 2 to signal {}".format(self.signaller.sig_dict[2]), flush=True)
        self.signaller.sig_dict[2].connect(self.ReceiveSignal2)
        print("Connecting key 9 to signal {}".format(self.signaller.sig_dict[9]), flush=True)
        self.signaller.sig_dict[9].connect(self.ReceiveSignal9)

    def ReceiveSignal2(self, info):
        print(info)

    def ReceiveSignal9(self, info):
        print(info)

signal.signal(signal.SIGINT, signal.SIG_DFL)
app = QtCore.QCoreApplication([])
signaller = Signaller()
receiver = Receiver(signaller)
thread = QtCore.QThread()
receiver.moveToThread(thread)
thread.start()
receiver.start_signal.emit()
#Trivial pause to provide time for receiver to set up connections
time.sleep(1)
signaller.start()
app.exec_()

我期望的是,当发出信号 2 时,它会定向到我连接的插槽。同样对于信号 9.

实际发生的是(你的里程可能会有所不同,我怀疑这是因为某些东西实际上不是线程安全的,尽管 PySide/Qt 文档建议连接是线程安全的)

C:\temp> pyside_signal_example.py
Connecting key 2 to signal <PySide.QtCore.SignalInstance object at 0x02C20C80>
Connecting key 9 to signal <PySide.QtCore.SignalInstance object at 0x02C20C50>
Emitting Key 0: Signal <PySide.QtCore.SignalInstance object at 0x02C20CA0>
Emitting Key 1: Signal <PySide.QtCore.SignalInstance object at 0x02C20CB0>
Emitting Key 2: Signal <PySide.QtCore.SignalInstance object at 0x02C20C80>
Emitting Key 3: Signal <PySide.QtCore.SignalInstance object at 0x02C20CE0>
Emitting Key 4: Signal <PySide.QtCore.SignalInstance object at 0x02C20CD0>
Emitting Key 5: Signal <PySide.QtCore.SignalInstance object at 0x02C20C70>
Emitting Key 6: Signal <PySide.QtCore.SignalInstance object at 0x02C20C90>
Key 5 for Signal <PySide.QtCore.SignalInstance object at 0x02C20C70>
Emitting Key 7: Signal <PySide.QtCore.SignalInstance object at 0x02C20C60>
Emitting Key 8: Signal <PySide.QtCore.SignalInstance object at 0x02C20CC0>
Emitting Key 9: Signal <PySide.QtCore.SignalInstance object at 0x02C20C50>
Key 9 for Signal <PySide.QtCore.SignalInstance object at 0x02C20C50>

请注意,我打印了信号的地址,当我建立连接并进行发射(以及在插槽中)时,键 9 的地址匹配,但是 "other" 信号连接到键 2 的插槽(这里是与键 5 关联的信号)与我尝试连接的信号的地址不匹配。

简答:

需要在 class 中声明信号,实际上不支持稍后分配它们*。

*它似乎在 PySide 中工作,但它选择了错误的信号来连接或发射。在 PyQt 中,尝试连接或发出此类信号时会立即失败。


更长的答案:

QObject 使用元class (Shiboken.ObjectType),它用于在定义一个时创建 class 个实例,那时 class 被检查是否设置正确。因此,当您稍后添加信号时,它们在那个时间点将不可用,因此无法正确设置它们。

如果您想动态分配信号,一种方法是创建一个派生自 ObjectType 的自定义元class,然后可以在创建实际 class 之前添加必要的信息。

示例:

...
# ObjectType is not directly accessible, so need to get as QObject's type
ObjectType = type(QtCore.QObject)
class SignallerMeta(ObjectType):

    def __new__(cls, name, parents, dct):
        sig_dct = {}   # also generate the sig_dict class attribute here
        for i in range(10):
            signal = QtCore.Signal(Information)
            sig_dct[i] = signal
            dct['signal_{}'.format(i)] = signal
        dct['sig_dict'] = sig_dct
        return super().__new__(cls, name, parents, dct)

class Signaller(QtCore.QThread, metaclass=SignallerMeta):

    def __init__(self, parent=None):
        super().__init__(parent)
        # no need to copy the signals here

    def run(self):
        for key, sig in self.sig_dict.items():
            print("Emitting Key {}: Signal {}".format(key, sig))
            sig.emit(Information(key, sig))

        self.exec_()
...