JZX轻语:简

[Qt/PyQt] 记录QThread使用的一次坑

发表于2023年10月25日

#Qt #PyQt #坑

在PyQt开发使用QThread的过程中,发现如果在worker(QObject对象)通过调用moveToThread移动到新的线程前,如果worker中有信号连接到了主线程的槽函数,那么这个槽函数即便会在worker移动到新线程之后,仍然在主线程中执行,而不是在worker所在的新线程中执行。这样会使得运行在主进程上的GUI程序在worker执行时会被阻塞,直到worker执行完毕。

问题重现

import sys
import time
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton


class Worker(QObject):
    finished = pyqtSignal()

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

    def run(self):
        print("Worker thread id: {}".format(int(QThread.currentThreadId())))
        for i in range(10):
            print(i)
            time.sleep(1)
        self.finished.emit()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.button = QPushButton(self, text="Text")
        self.setCentralWidget(self.button)

        self.worker = Worker()
        self.worker.finished.connect(self.on_worker_finished)
        self.worker_thread = QThread()
        self.worker_thread.started.connect(self.worker.run)  # !!! 1
        self.worker.moveToThread(self.worker_thread)  # !!! 2
        print("Main thread id: {}".format(int(QThread.currentThreadId())))

    @pyqtSlot()
    def on_worker_finished(self):
        print("Worker finished")

    def startWorker(self):
        self.worker_thread.start()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    window.startWorker()
    sys.exit(app.exec_())

运行程序可以看到,GUI程序中(包括其中的按钮)无法响应鼠标事件,直到worker执行完毕。通过命令行输出也可以看到,worker.run方法所输出的线程id和主线程id是一样的,这意味着worker.run方法是在主线程中执行的,从而阻塞了GUI程序的运行。

稍微调整下代码顺序如下:

# 前面的代码省略 

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.button = QPushButton(self, text="Text")
        self.setCentralWidget(self.button)

        self.worker = Worker()
        self.worker_thread = QThread()
        self.worker.moveToThread(self.worker_thread)  # !!! 2

        self.worker.finished.connect(self.on_worker_finished)
        self.worker_thread.started.connect(self.worker.run)  # !!! 1
        print("Main thread id: {}".format(int(QThread.currentThreadId())))

# 后面的代码省略

此时可以看到,GUI程序可以正常响应鼠标事件,且命令行输出也可以看到,worker.run方法所输出的线程id和主线程id是不一样的,此时worker.run方法才是真正地异步执行的。

问题分析

为什么会出现这种情况呢?我们可以研究下信号和槽的连接方式,比如:

我们注意到,在第一份代码中,worker.run方法是在worker.moveToThread调用前连接到了worker_thread.started信号,由于连接的时候位于主线程,因此该槽所在的线程属于主进程,通过Auto Connectionstarted信号绑定。因此started信号发出后,将以Queued Connection的连接类型(注意连接类型在信号发出时确定)通知给槽,worker.run方法会在主线程执行,阻塞了GUI程序。

因此,需要在绑定信号和槽的时候,确保槽所在的线程是目前worker所在的线程,即确保workermoveToThread调用位于和QThread信号的连接之前。

总结

参考

上一篇

联通SK-D742-C光猫管理员密码获取/宽带密码获取/改桥接/IPv6设置

下一篇

[LeetCode每日一题] 2952需要添加的银币的最小数量