在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
方法才是真正地异步执行的。
问题分析
为什么会出现这种情况呢?我们可以研究下信号和槽的连接方式,比如:
Auto Connection
(默认):如果信号在接收对象具有密切关系的线程中发出,则行为与Direct Connection
相同。否则,行为与Queued Connection
相同。连接类型在信号发出时确定。Direct Connection
:当信号发出时,该槽会立即被调用。槽在发射器的线程中执行,该线程不一定是接收器的线程。Queued Connection
:当控制返回到接收者线程的事件循环时,将调用该槽。该槽在接收者的线程中执行。
我们注意到,在第一份代码中,worker.run
方法是在worker.moveToThread
调用前连接到了worker_thread.started
信号,由于连接的时候位于主线程,因此该槽所在的线程属于主进程,通过Auto Connection
和started
信号绑定。因此started
信号发出后,将以Queued Connection
的连接类型(注意连接类型在信号发出时确定)通知给槽,worker.run
方法会在主线程执行,阻塞了GUI程序。
因此,需要在绑定信号和槽的时候,确保槽所在的线程是目前worker所在的线程,即确保worker
的moveToThread
调用位于和QThread
信号的连接之前。
总结
-
槽所在的线程由信号连接(connect)时所在的线程决定,而不是信号发出时所在的线程,也不是所在对象的线程。
-
如果
connect
不指定连接类型,那么连接类型由信号发出时所在的线程决定。
参考
-
StackOverflow: Qt signals (QueuedConnection and DirectConnection)
-
StackOverflow: Qt signaling across threads, one is GUI thread?