PAYATHON 2026

QThread 设置 parent=self 后关闭窗口报错,如何正确退出线程?

数据小陈

结论

parent=self 只改变 QThread 对象的所有权,Worker 完成时线程不会自动退出。原代码没有调用 quit(),也没有等待线程结束,因此重新启动任务或关闭窗口时,仍可能销毁一个正在运行的 QThread。

需要处理以下几件事:

  1. Worker 完成后调用线程的 quit()。
  2. 线程真正结束后,再恢复启动按钮并清理对象引用。
  3. 关闭窗口时请求任务停止,调用 quit(),并等待线程结束。

原代码的 Stop 信号也可能无法及时生效:do_work() 长时间占用工作线程,发给该线程的停止槽可能要等循环结束后才能执行。这个示例可以改用 requestInterruption(),让 Worker 在循环中主动检查停止请求。

为什么 parent=self 会改变报错时机?

Worker 完成不等于线程结束

原代码末尾执行的是:

self.finished.emit()

这是自定义的 Worker.finished 信号。目前它只连接窗口的 finished() 方法,用于更新按钮和日志,不会让 QThread 退出。

do_work() 返回后,QThread 默认运行的事件循环仍可继续等待事件。因此,即使窗口显示 Finished,线程也可能仍在运行。

Worker.finished 和 QThread.finished 是不同的信号:前者由业务代码发出,后者表示线程执行即将结束。

不设置 parent 时,重新赋值可能销毁旧线程

再次点击 Start 会执行:

self.worker_thread = QThread()

这会覆盖之前保存的 Python 引用。如果旧 QThread 没有父对象,也没有其他引用维持其生命周期,它可能在此时被销毁。如果旧线程仍在运行,就会出现:

QThread: Destroyed while thread is still running

设置 parent 后,旧线程由窗口持有

使用:

self.worker_thread = QThread(parent=self)

窗口会持有这些 QThread 子对象。覆盖 self.worker_thread 不再立即导致旧对象被销毁,所以重复启动看起来正常,但旧线程可能一直留着。

窗口销毁时,其 QThread 子对象也会被销毁,错误便转移到了关闭窗口的阶段。

报错时机还会受到对象引用和事件处理时序的影响,不能因为偶尔不报错就认为线程已经结束。

为什么原来的 Stop 可能无效?

Worker 已通过 moveToThread() 移到工作线程。点击 Stop 后,从 GUI 线程发出的信号通常以队列方式交给工作线程执行:

self.worker.stop.emit()

但工作线程正在执行整个 do_work() 循环,无法同时处理排队的 request_stop() 调用。

其中的:

time.sleep(self.delay)

只会暂停当前线程,不会处理 Qt 事件。因此,停止槽可能要等任务自然结束后才有机会运行。

可以从 GUI 线程通过 requestInterruption() 发出中断请求,让 Worker 在循环中用 isInterruptionRequested() 检查,无需等待停止槽执行。这是协作式停止,Worker 必须主动响应。

可直接运行的修改示例

示例每次启动都会创建新的 QThread 和 Worker,直到收到 QThread.finished 后才允许再次启动。

import time

from PyQt6.QtCore import QObject, QThread, Qt, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
    QApplication,
    QPlainTextEdit,
    QPushButton,
    QVBoxLayout,
    QWidget,
)


class Worker(QObject):
    finished = pyqtSignal()
    progress = pyqtSignal(int)

    def __init__(self, start_num, end_num, delay):
        super().__init__()
        self.start_num = start_num
        self.end_num = end_num
        self.delay = delay

    @pyqtSlot()
    def do_work(self):
        thread = QThread.currentThread()
        try:
            for i in range(self.start_num, self.end_num + 1):
                if thread.isInterruptionRequested():
                    break
                self.progress.emit(i)
                time.sleep(self.delay)
        finally:
            self.finished.emit()


class MyWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.worker_thread = None
        self.worker = None

        self.progress_box = QPlainTextEdit()
        self.progress_box.setFixedHeight(100)

        self.start_btn = QPushButton("Start")
        self.stop_btn = QPushButton("Stop")
        self.stop_btn.setVisible(False)

        self.start_btn.clicked.connect(self.start_worker)
        self.stop_btn.clicked.connect(self.stop_worker)

        layout = QVBoxLayout(self)
        layout.addWidget(self.progress_box)
        layout.addWidget(
            self.start_btn, alignment=Qt.AlignmentFlag.AlignCenter
        )
        layout.addWidget(
            self.stop_btn, alignment=Qt.AlignmentFlag.AlignCenter
        )

    @pyqtSlot()
    def start_worker(self):
        # 等待上一轮线程结束及清理回调完成。
        if self.worker_thread is not None:
            return

        self.progress_box.appendPlainText("Starting")
        self.start_btn.setVisible(False)
        self.stop_btn.setEnabled(True)
        self.stop_btn.setVisible(True)

        thread = QThread(parent=self)
        worker = Worker(1, 10, 0.5)
        worker.moveToThread(thread)

        self.worker_thread = thread
        self.worker = worker

        thread.started.connect(worker.do_work)
        worker.progress.connect(self.update_progress)
        worker.finished.connect(thread.quit)

        thread.finished.connect(worker.deleteLater)
        thread.finished.connect(self.on_thread_finished)
        thread.finished.connect(thread.deleteLater)

        thread.start()

    @pyqtSlot()
    def stop_worker(self):
        thread = self.worker_thread
        if thread is not None and thread.isRunning():
            thread.requestInterruption()
            self.stop_btn.setEnabled(False)

    @pyqtSlot(int)
    def update_progress(self, value):
        self.progress_box.appendPlainText(str(value))

    @pyqtSlot()
    def on_thread_finished(self):
        self.worker = None
        self.worker_thread = None
        self.progress_box.appendPlainText("Finished")
        self.start_btn.setVisible(True)
        self.stop_btn.setVisible(False)

    def closeEvent(self, event):
        thread = self.worker_thread
        if thread is not None:
            thread.requestInterruption()
            thread.quit()
            thread.wait()
        event.accept()


if __name__ == "__main__":
    app = QApplication([])
    win = MyWindow()
    win.show()
    app.exec()

示例保留了 parent=self。给 QThread 设置父对象本身没有问题,只要确保线程在窗口销毁前结束即可。

退出和清理时要注意什么?

quit() 不会打断正在执行的循环

quit() 请求线程的事件循环退出,不能强制让正在执行的 do_work() 返回。停止工作依赖 Worker 检查中断标志。

本例停止时通常需要等当前 time.sleep(0.5) 返回。如果实际工作包含耗时计算、网络请求或其他阻塞操作,需要在合适的位置检查中断,并为阻塞操作设置超时或使用其取消机制。

关闭窗口时先调用 quit(),再 wait()

QThread 对象本身仍属于 GUI 线程。正常运行时,worker.finished.connect(thread.quit) 通常通过 GUI 事件循环调度 quit()。

如果关闭窗口时只调用 wait(),GUI 线程会阻塞,排队的 quit() 可能无法执行,造成互相等待。因此,示例在 closeEvent() 中直接调用 quit(),然后再调用 wait()。

这仍然要求 Worker 能自行返回。对于退出时间不可控的任务,不宜在 GUI 线程无限等待;可以先忽略关闭事件,发出停止请求,在收到 QThread.finished 后再关闭窗口。如果使用带超时的 wait(),超时后也不能继续销毁仍在运行的线程。

在线程结束后清理对象

示例使用:

thread.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)

第一条连接安排 Worker 的清理,第二条安排 QThread 对象的清理。窗口收到线程结束信号后,再清空保存的 Python 引用并恢复按钮。

deleteLater() 负责对象销毁,不负责停止线程。也不建议用 terminate() 作为常规停止方案,因为它可能在资源尚未释放时强制终止执行。

备注:内容仅供参考。