PySide6工作线程不响应主线程槽调用的问题排查与解决
主线程向Qt工作线程实时发送数据的问题排查与解决
问题描述
基于Python+Qt开发项目时,为避免GUI冻结将耗时任务移至独立工作线程,希望线程运行期间能定期响应主线程的新数据指令。编写测试Demo后发现:点击「Print to console!」按钮触发的Set print_to_console to true日志,要等工作线程的run函数执行完毕后才打印,说明run函数阻塞了工作线程的事件处理,主线程的指令无法实时响应。
测试代码如下:
import sys from time import sleep import shiboken6 from PySide6.QtCore import Qt, QObject, QThread, Signal, Slot, QTimer from PySide6.QtWidgets import ( QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget, ) class Worker(QObject): finished = Signal() progress = Signal(int) def __init__(self): super().__init__() self.print_to_console_plz = False @Slot() def print_on_console_while_running(self): self.print_to_console_plz = True print("Set print_to_console to true") def run(self): timer = QTimer() for i in range(5): sleep(0.9) timer.start(100) if self.print_to_console_plz: print("Hello World from worker") self.print_to_console_plz = False self.progress.emit(i + 1) self.finished.emit() class Window(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.clicksCount = 0 self.initWorker() self.setupUi() def initWorker(self): self.thread = QThread() self.worker = Worker() self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.worker.progress.connect(self.reportProgress) def setupUi(self): self.setWindowTitle("Freezing GUI") self.resize(300, 150) self.centralWidget = QWidget() self.setCentralWidget(self.centralWidget) self.clicksLabel = QLabel("Counting: 0 clicks", self) self.clicksLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.stepLabel = QLabel("Long-Running Step: 0") self.stepLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.clicksToConsoleLabel = QLabel("Click here to print to console", self) self.clicksToConsoleLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.countBtn = QPushButton("Click me!", self) self.countBtn.clicked.connect(self.countClicks) self.ToConsoleBttn = QPushButton("Print to console!", self) self.ToConsoleBttn.clicked.connect(self.worker.print_on_console_while_running) self.longRunningBtn = QPushButton("Long-Running Task!", self) self.longRunningBtn.clicked.connect(self.runLongTask) layout = QVBoxLayout() layout.addWidget(self.clicksLabel) layout.addWidget(self.countBtn) layout.addStretch() layout.addWidget(self.clicksToConsoleLabel) layout.addWidget(self.ToConsoleBttn) layout.addStretch() layout.addWidget(self.stepLabel) layout.addWidget(self.longRunningBtn) self.centralWidget.setLayout(layout) def countClicks(self): self.clicksCount += 1 self.clicksLabel.setText(f"Counting: {self.clicksCount} clicks") def reportProgress(self, n): self.stepLabel.setText(f"Long-Running Step: {n}") def runLongTask(self): if not shiboken6.isValid(self.thread): self.initWorker() self.ToConsoleBttn.clicked.connect(self.worker.print_on_console_while_running) self.thread.start() self.longRunningBtn.setEnabled(False) self.thread.finished.connect( lambda: self.longRunningBtn.setEnabled(True) ) self.thread.finished.connect( lambda: self.stepLabel.setText("Long-Running Step: 0") ) app = QApplication(sys.argv) win = Window() win.show() sys.exit(app.exec())
错误原因分析
- QTimer未正确利用事件循环:在
run函数中创建的QTimer没有绑定到工作线程的事件循环,且start(100)仅单次触发,无法驱动事件循环运转。 - 阻塞式代码占用线程:
run函数中的sleep(0.9)和for循环是同步阻塞逻辑,完全占用了工作线程的执行资源,导致线程的事件循环无法处理主线程发来的信号对应的槽函数(print_on_console_while_running),只能等run函数执行完毕后才会处理积压的事件。
解决方案
要实现工作线程同时处理耗时任务和主线程指令,必须保证工作线程的事件循环正常运转,以下是两种可行方案:
方案一:用QTimer驱动耗时任务(推荐)
将循环拆分到QTimer的槽函数中,让事件循环有机会处理其他信号:
import sys import shiboken6 from PySide6.QtCore import Qt, QObject, QThread, Signal, Slot, QTimer from PySide6.QtWidgets import ( QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget, ) class Worker(QObject): finished = Signal() progress = Signal(int) def __init__(self): super().__init__() self.print_to_console_plz = False self.current_step = 0 self.task_timer = QTimer() self.task_timer.setInterval(900) # 对应原sleep(0.9)的间隔 self.task_timer.timeout.connect(self.execute_task_step) @Slot() def print_on_console_while_running(self): self.print_to_console_plz = True print("Set print_to_console to true") @Slot() def start_task(self): self.current_step = 0 self.task_timer.start() @Slot() def execute_task_step(self): # 响应主线程指令 if self.print_to_console_plz: print("Hello World from worker") self.print_to_console_plz = False # 推进任务进度 self.current_step += 1 self.progress.emit(self.current_step) # 任务结束判断 if self.current_step >= 5: self.task_timer.stop() self.finished.emit() class Window(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.clicksCount = 0 self.initWorker() self.setupUi() def initWorker(self): self.thread = QThread() self.worker = Worker() self.worker.moveToThread(self.thread) # 修改信号连接:线程启动后触发任务开始 self.thread.started.connect(self.worker.start_task) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.worker.progress.connect(self.reportProgress) def setupUi(self): self.setWindowTitle("Non-Freezing GUI") self.resize(300, 150) self.centralWidget = QWidget() self.setCentralWidget(self.centralWidget) self.clicksLabel = QLabel("Counting: 0 clicks", self) self.clicksLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.stepLabel = QLabel("Long-Running Step: 0") self.stepLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.clicksToConsoleLabel = QLabel("Click here to print to console", self) self.clicksToConsoleLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.countBtn = QPushButton("Click me!", self) self.countBtn.clicked.connect(self.countClicks) self.ToConsoleBttn = QPushButton("Print to console!", self) self.ToConsoleBttn.clicked.connect(self.worker.print_on_console_while_running) self.longRunningBtn = QPushButton("Long-Running Task!", self) self.longRunningBtn.clicked.connect(self.runLongTask) layout = QVBoxLayout() layout.addWidget(self.clicksLabel) layout.addWidget(self.countBtn) layout.addStretch() layout.addWidget(self.clicksToConsoleLabel) layout.addWidget(self.ToConsoleBttn) layout.addStretch() layout.addWidget(self.stepLabel) layout.addWidget(self.longRunningBtn) self.centralWidget.setLayout(layout) def countClicks(self): self.clicksCount += 1 self.clicksLabel.setText(f"Counting: {self.clicksCount} clicks") def reportProgress(self, n): self.stepLabel.setText(f"Long-Running Step: {n}") def runLongTask(self): if not shiboken6.isValid(self.thread): self.initWorker() self.ToConsoleBttn.clicked.connect(self.worker.print_on_console_while_running) self.thread.start() self.longRunningBtn.setEnabled(False) self.thread.finished.connect(lambda: self.longRunningBtn.setEnabled(True)) self.thread.finished.connect(lambda: self.stepLabel.setText("Long-Running Step: 0")) app = QApplication(sys.argv) win = Window() win.show() sys.exit(app.exec())
方案二:在循环中手动处理事件(不推荐)
如果必须保留同步循环,可以在循环中调用QThread.processEvents()强制处理待办事件,但可能引发槽函数重入等问题:
def run(self): for i in range(5): sleep(0.9) # 手动处理线程积压事件,让槽函数有机会执行 QThread.processEvents() if self.print_to_console_plz: print("Hello World from worker") self.print_to_console_plz = False self.progress.emit(i + 1) self.finished.emit()
关键注意事项
- Qt跨线程信号的处理依赖线程的事件循环,工作线程中绝对不能有长时间阻塞的同步代码,否则会导致信号响应延迟。
- 跨线程传递数据的规范方式是信号-槽机制:主线程发射携带数据的信号,Worker定义对应槽函数接收,Qt会自动处理线程间的同步,避免线程安全问题(例如可以给Worker添加
print_requested信号,替代直接修改属性的方式)。
内容的提问来源于stack exchange,提问作者arc_lupus
相关产品推荐
相关产品推荐

