QThread 设置 parent=self 后关闭窗口报错,如何正确退出线程?
结论
parent=self 只改变 QThread 对象的所有权,Worker 完成时线程不会自动退出。原代码没有调用 quit(),也没有等待线程结束,因此重新启动任务或关闭窗口时,仍可能销毁一个正在运行的 QThread。
需要处理以下几件事:
- Worker 完成后调用线程的
quit()。 - 线程真正结束后,再恢复启动按钮并清理对象引用。
- 关闭窗口时请求任务停止,调用
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() 作为常规停止方案,因为它可能在资源尚未释放时强制终止执行。
备注:内容仅供参考。
