为何PyQt子线程执行繁重重复任务时会冻结GUI?
问题:移到QThread的QTimer任务仍导致GUI冻结
我实现了一个继承QObject的Worker类,用QTimer每秒重复执行一个耗时0.5秒的任务,并将Worker移至QThread中。按理论,任务应在新线程运行,不会干扰GUI主线程,但实际GUI会反复冻结。若移除sleep()或缩短其时长,GUI恢复正常,但我实际需要处理更长耗时的任务,这成了难题。
问题代码
import sys from time import sleep from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject, QTimer from PyQt5.QtWidgets import ( QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget, ) class Worker(QObject): progress = pyqtSignal(int) def __init__(self): super().__init__() self.i=0 self.Timer=QTimer(self) self.Timer.timeout.connect(self.run) self.Timer.setInterval(1000) def run(self): """Long-running task.""" sleep(0.5) self.i=self.i+1 self.progress.emit(self.i) class Window(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.clicksCount = 0 self.setupUi() def setupUi(self): self.setWindowTitle("Freezing GUI") self.resize(300, 150) self.centralWidget = QWidget() self.setCentralWidget(self.centralWidget) # Create and connect widgets 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.countBtn = QPushButton("Click me!", self) self.countBtn.clicked.connect(self.countClicks) self.longRunningBtn = QPushButton("Long-Running Task!", self) self.longRunningBtn.clicked.connect(self.runLongTask) # Set the layout layout = QVBoxLayout() layout.addWidget(self.clicksLabel) layout.addWidget(self.countBtn) 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): # Step 2: Create a QThread object self.thread = QThread() # Step 3: Create a worker object self.worker = Worker() # Step 4: Move worker to the thread self.worker.moveToThread(self.thread) # Step 5: Connect signals and slots self.thread.started.connect(self.worker.Timer.start) self.thread.finished.connect(self.thread.deleteLater) self.worker.progress.connect(self.reportProgress) # Step 6: Start the thread self.thread.start() self.longRunningBtn.setEnabled(False) app = QApplication(sys.argv) win = Window() win.show() sys.exit(app.exec())
原因分析
问题出在QTimer的线程亲和性:
- 创建Worker时,QTimer是在主线程初始化的(此时Worker还未移到子线程),因此QTimer的线程绑定关系是主线程。
- 虽然后续将Worker移到了子线程,但QTimer作为Worker的子对象,其线程绑定关系变更后会被自动停止,且无法跨线程正常运行。
- 最终
self.worker.Timer.start()实际是在主线程启动定时器,run()方法会在主线程执行,sleep(0.5)阻塞了主线程的事件循环,导致GUI冻结。
解决方案
方案1:在子线程中初始化QTimer
将QTimer的初始化移到子线程执行的方法中,确保定时器的线程亲和性与子线程绑定:
import sys from time import sleep from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject, QTimer from PyQt5.QtWidgets import ( QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget, ) class Worker(QObject): progress = pyqtSignal(int) def __init__(self): super().__init__() self.i=0 self.Timer = None # 先不初始化定时器 def init_and_start_timer(self): # 此方法在子线程执行,确保QTimer创建在子线程 self.Timer=QTimer(self) self.Timer.timeout.connect(self.run) self.Timer.setInterval(1000) self.Timer.start() def run(self): """Long-running task.""" sleep(0.5) self.i=self.i+1 self.progress.emit(self.i) class Window(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.clicksCount = 0 self.setupUi() def setupUi(self): self.setWindowTitle("Fixed Freezing GUI") self.resize(300, 150) self.centralWidget = QWidget() self.setCentralWidget(self.centralWidget) # Create and connect widgets 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.countBtn = QPushButton("Click me!", self) self.countBtn.clicked.connect(self.countClicks) self.longRunningBtn = QPushButton("Long-Running Task!", self) self.longRunningBtn.clicked.connect(self.runLongTask) # Set the layout layout = QVBoxLayout() layout.addWidget(self.clicksLabel) layout.addWidget(self.countBtn) 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): self.thread = QThread() self.worker = Worker() self.worker.moveToThread(self.thread) # 线程启动时,在子线程中初始化并启动定时器 self.thread.started.connect(self.worker.init_and_start_timer) self.thread.finished.connect(self.thread.deleteLater) self.worker.progress.connect(self.reportProgress) self.thread.start() self.longRunningBtn.setEnabled(False) app = QApplication(sys.argv) win = Window() win.show() sys.exit(app.exec())
方案2:不用QTimer,直接在子线程循环执行任务
如果不需要精确的定时器,可直接在子线程中通过循环+线程睡眠实现定时任务,避免QTimer的线程绑定问题:
import sys from time import sleep from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject from PyQt5.QtWidgets import ( QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget, ) class Worker(QObject): progress = pyqtSignal(int) def __init__(self): super().__init__() self.i=0 self.running = True def run_task_loop(self): while self.running: # 执行耗时任务 sleep(0.5) self.i=self.i+1 self.progress.emit(self.i) # 间隔1秒后再次执行 QThread.sleep(1) def stop(self): self.running = False class Window(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.clicksCount = 0 self.setupUi() def setupUi(self): self.setWindowTitle("Fixed Freezing GUI") self.resize(300, 150) self.centralWidget = QWidget() self.setCentralWidget(self.centralWidget) # Create and connect widgets 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.countBtn = QPushButton("Click me!", self) self.countBtn.clicked.connect(self.countClicks) self.longRunningBtn = QPushButton("Long-Running Task!", self) self.longRunningBtn.clicked.connect(self.runLongTask) # 添加停止按钮 self.stopBtn = QPushButton("Stop Task!", self) self.stopBtn.clicked.connect(self.stopLongTask) self.stopBtn.setEnabled(False) # Set the layout layout = QVBoxLayout() layout.addWidget(self.clicksLabel) layout.addWidget(self.countBtn) layout.addStretch() layout.addWidget(self.stepLabel) layout.addWidget(self.longRunningBtn) layout.addWidget(self.stopBtn) 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): self.thread = QThread() self.worker = Worker() self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run_task_loop) self.thread.finished.connect(self.thread.deleteLater) self.worker.progress.connect(self.reportProgress) self.thread.start() self.longRunningBtn.setEnabled(False) self.stopBtn.setEnabled(True) def stopLongTask(self): self.worker.stop() self.thread.quit() self.thread.wait() self.longRunningBtn.setEnabled(True) self.stopBtn.setEnabled(False) app = QApplication(sys.argv) win = Window() win.show() sys.exit(app.exec())
内容的提问来源于stack exchange,提问作者WEINAN WANG
相关产品推荐
相关产品推荐

