在Python线程中使用QObject发射信号

8
我想知道在一个QObject内部从常规的Python线程发出信号与在QThread中发出信号有什么后果。
请参考以下类:
class MyObject(QtCore.QObject):

    def __init__(self):
        super().__init__()

    sig = pyqtSignal()

    def start(self):
        self._thread = Thread(target=self.run)
        self._thread.start()

    def run(self):
        self.sig.emit()
        # Do something

假设在GUI线程中,我有以下代码:

def __init__(self):
    self.obj = MyObject()
    self.obj.sig.connect(self.slot)
    self.obj.start()

def slot(self):
    # Do something

当信号被发射时,slot确实会被执行。但是,我想知道slot方法将在哪个线程中执行?如果在MyObject中使用QThread而不是Python线程,是否会有任何区别?

我正在使用PyQt5和Python 3。

2个回答

11

Qt默认情况下,当跨线程发出信号时,会自动将其排队。为此,它序列化信号参数,然后在接收线程的事件队列中发布一个事件,其中任何连接的槽最终都会被执行。以这种方式发出的信号因此保证是线程安全的。

关于外部线程,Qt文档说明如下:

注意: Qt的线程类使用了本地线程API实现;例如Win32和pthreads。因此,它们可以与同一本地API的线程一起使用。

通常,如果文档指出Qt的API是线程安全的,那么该保证适用于使用相同本地库创建的所有线程 - 而不仅仅是由Qt本身创建的线程。这意味着也可以使用这些线程安全的API(如postEvent()invoke())明确地向其他线程发布事件。

因此,在发出跨线程信号时,使用threading.ThreadQThread之间实际上没有真正的区别,只要Python和Qt使用相同的底层本地线程库。这表明,在PyQt应用程序中首选使用QThread的一个可能原因是可移植性,因为这样就不会有混合不兼容线程实现的危险。然而,在实践中,这个问题极不可能出现,因为Python和Qt都是有意设计成跨平台的。
关于slot将在哪个线程中执行的问题,对于Python和Qt来说,它将在线程中执行。相比之下,run方法将在工作线程中执行。这是在Qt应用程序中进行多线程时非常重要的考虑因素,因为在主线程外执行gui操作是不安全的。使用信号允许您在工作者线程和gui之间安全地通信,因为连接到从工作者发出的信号的插槽将在主线程中调用,如果需要,可以在那里更新gui。
下面是一个简单的脚本,显示了每个方法调用的线程:
import sys, time, threading
from PyQt5 import QtCore, QtWidgets

def thread_info(msg):
    print(msg, int(QtCore.QThread.currentThreadId()),
          threading.current_thread().name)

class PyThreadObject(QtCore.QObject):
    sig = QtCore.pyqtSignal()

    def start(self):
        self._thread = threading.Thread(target=self.run)
        self._thread.start()

    def run(self):
        time.sleep(1)
        thread_info('py:run')
        self.sig.emit()

class QtThreadObject(QtCore.QThread):
    sig = QtCore.pyqtSignal()

    def run(self):
        time.sleep(1)
        thread_info('qt:run')
        self.sig.emit()

class Window(QtWidgets.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.pyobj = PyThreadObject()
        self.pyobj.sig.connect(self.pyslot)
        self.pyobj.start()
        self.qtobj = QtThreadObject()
        self.qtobj.sig.connect(self.qtslot)
        self.qtobj.start()

    def pyslot(self):
        thread_info('py:slot')

    def qtslot(self):
        thread_info('qt:slot')

if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.setGeometry(600, 100, 300, 200)
    window.show()
    thread_info('main')
    sys.exit(app.exec_())

输出:

main 140300376593728 MainThread
py:run 140299947104000 Thread-1
py:slot 140300376593728 MainThread
qt:run 140299871450880 Dummy-2
qt:slot 140300376593728 MainThread

从您提供的链接中可以得知:在Python线程中无法使用Qt(例如,无法通过QApplication.postEvent将事件发送到主线程),因此需要使用QThread才能实现该功能。由于emit()调用了postEvent(),因此如果我们相信您提供的链接,那么我们不能从Python线程中使用.emit()方法。 - jfs
1
@jfs。如果您阅读了那个答案的评论,您会发现那个说法是完全错误的。没有任何证据支持它,所以您可以放心地忽略它。 - ekhumoro
2
@jfs。由于它已经有些老旧且有争议,因此我已从我的答案中删除了链接并添加了一些全新的内容。我本来希望找到更完整的官方Qt声明关于外部线程的问题,但我觉得我找到的这个声明已经很清楚了。 - ekhumoro

0

我想要添加:

class MyQThread(QThread):
    signal = pyqtSignal() # This thread emits this at some point.

class MainThreadObject(QObject):
    def __init__(self):
        thread = MyQThread()
        thread.signal.connect(self.mainThreadSlot)
        thread.start()

    @pyqtSlot()
    def mainThreadSlot(self):
        pass

根据我所知的所有文档,这是完全可以的。以下也是如此:

class MyQObject(QObject):
    signal = pyqtSignal()

class MainThreadObject(QObject):
    def __init__(self):
        self.obj = MyQObject()
        self.obj.signal.connect(self.mainThreadSlot)
        self.thread = threading.Thread(target=self.callback)
        self.thread.start()

    def callback(self):
        self.obj.signal.emit()

    @pyqtSlot()
    def mainThreadSlot(self):
        pass

从@ekhumoro的说法来看,这两个在功能上是相同的。因为QThread只是一个QObject,它的run()方法是一个threading.Thread的目标=。

换句话说,MyQThread和MyQObject的信号都是由主线程“拥有”的内存,但是可以从子线程中访问。

因此,以下内容也应该是安全的:

class MainThreadObject(QObject):
    signal = pyqtSignal() # Connect to this signal from QML or Python

    def __init__(self):
        self.thread = threading.Thread(target=self.callback)
        self.thread.start()

    def callback(self):
        self.signal.emit()

如果我错了,请纠正我。从Qt和/或Riverbank获得关于这种行为的官方文档将非常好。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接