You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 23:27:00