Python Qt - 是否需要在工作线程中创建信号?

Python Qt - do signals need to be created in the worker thread?

我正在使用 Python Qt (PySide2),它在主线程中有一个 GUI,在第二个线程中有一个执行 IO、数据处理等的库。

我使用信号和槽来更新 GUI。在我在 SO 上看到的示例中,信号始终在工作线程(非 GUI)中创建。有必要这样做吗?

原因:我的库可以与 GUI 一起使用,也可以在另一个 Python 脚本中使用。因此,它可能会将数据输出到 GUI 或者 console/log 文件。为了使库中的代码通用,我认为无论调用库都可以注册一个回调。该回调可以是 emit 到 Qt 或输出到文件等

这是一个在 GUI 线程中创建信号的示例。它有效,但它会导致与线程相关的问题吗?

import threading
import time
import sys
from PySide2.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QPushButton, QApplication
from PySide2 import QtCore

class aio_connection(QtCore.QObject):
    data_recvd = QtCore.Signal(object) 

class TextEditDemo(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.setWindowTitle("TEST")
        self.resize(600,540)

        self.textEdit = QTextEdit()
        self.btnPress1 = QPushButton("Run")

        layout = QVBoxLayout()
        layout.addWidget(self.textEdit)
        layout.addWidget(self.btnPress1)
        self.setLayout(layout)

        self.btnPress1.clicked.connect(self.btnPress1_Clicked)

        self.aio_conn = aio_connection()    # Signal is created in main (GUI) thread

        # Connect signals (data_recvd) and slots (on_data_ready)
        self.aio_conn.data_recvd.connect(self.on_data_ready)

    def btnPress1_Clicked(self):
        threading.Thread(target=bg_task, args=(self.cb,)).start() 

    @QtCore.Slot()
    def on_data_ready(self, msg):
        self.textEdit.append(msg)

    def cb(self, info):
        self.aio_conn.data_recvd.emit(info)

# Simulate the library that runs in a second thread
def bg_task(callback):
    for i in range(100):
        callback(str(i))
        time.sleep(0.1)

def main():
    app = QApplication(sys.argv)
    win = TextEditDemo()
    win.show()

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

默认情况下,信号连接使用AutoConnection类型。这意味着最终类型的连接将在 运行time 时自动建立,当信号实际发出时。如果发送者和接收者在不同的线程中,Qt 将使用排队连接,这将 post 事件发送到接收线程的 event-queue。因此,当控制 returns 到接收者时,事件将被处理,任何连接的槽都将在那时被调用。

从工作线程更新 GUI 元素时,这是一个重要的考虑因素,因为 Qt 不支持 GUI-related 主线程之外的任何类型的操作。但是,只要您始终使用信号在使用默认 connection-type 的线程之间进行通信,您就不会有任何问题 - Qt 会自动保证它们以 thread-safe 方式完成。

以下是您的脚本版本,可验证一切是否按预期运行。当我 运行 它时,我得到以下输出,显示每个函数调用中的当前 thread-id:

main: 4973
bg_task: 4976
cb: 4976
on_data_ready: 4973
cb: 4976
on_data_ready: 4973
cb: 4976
...
import threading, time, sys
from PySide2.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QPushButton, QApplication
from PySide2 import QtCore

class aio_connection(QtCore.QObject):
    data_recvd = QtCore.Signal(object)

class TextEditDemo(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("TEST")
        self.resize(600,540)
        self.textEdit = QTextEdit()
        self.btnPress1 = QPushButton("Run")
        layout = QVBoxLayout()
        layout.addWidget(self.textEdit)
        layout.addWidget(self.btnPress1)
        self.setLayout(layout)
        self.btnPress1.clicked.connect(self.btnPress1_Clicked)
        self.aio_conn = aio_connection()
        self.aio_conn.data_recvd.connect(self.on_data_ready)

    def btnPress1_Clicked(self):
        threading.Thread(target=bg_task, args=(self.cb,)).start()

    @QtCore.Slot()
    def on_data_ready(self, msg):
        print('on_data_ready:', threading.get_native_id())
        self.textEdit.append(msg)

    def cb(self, info):
        print('cb:', threading.get_native_id())
        self.aio_conn.data_recvd.emit(info)

def bg_task(callback):
    print('bg_task:', threading.get_native_id())
    for i in range(5):
        callback(str(i))
        time.sleep(0.1)

def main():
    print('main:', threading.get_native_id())
    app = QApplication(sys.argv)
    win = TextEditDemo()
    win.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()