线程不仅要能启动,还要能正确退出。 否则容易出现 内存泄漏、僵尸线程、甚至程序崩溃。
在上一章节《01:QThread 基础》中,我们学习了如何用 Worker + QThread 的模式把耗时任务放到子线程执行,并通过信号把结果传回 UI,从而保证界面不卡顿。那一章的重点是 "能跑起来"。
但是,线程的生命周期不仅仅是"启动和执行"。如果我们只考虑启动,而忽略了安全退出,就可能出现以下问题:
- 任务中途退出困难:比如用户点击"停止"按钮,但子线程仍在继续工作。
- 资源无法释放:线程对象或 Worker 对象没有被销毁,导致内存泄漏。
- 强行终止风险:直接调用 terminate() 会让线程在任意位置被中断,可能破坏内存或数据。
因此,本章将重点讲解QThread 的生命周期与安全退出。 我们会在上一章的基础上,增加可控的停止机制 ,让子线程能在任务中途被优雅地结束,并且在退出时正确释放资源。

如上所示:
- 前端QML跟后端Pyside6的交互一共有4处:
- 按钮->开始任务;
- 按钮->停止任务;
- 进度条->任务的进度;
- 显示文本->任务的结果;
- Worker类与Backend类都有两个信号。因为后端QML只认识Backend类,不认识Worker类。所以,Worker类的信号只能通过Backend类的信号传递给后端QML。
- Worker类信号 -> Backend类信号 -> QML
- Worker类的finished信号稍微复杂一些,它触发多个函数。例如,清理线程,任务标志位等等。
效果如下所示:

工程代码:
- github: https://github.com/q164129345/myPyside6_QML/tree/main/thread02_safe_quit
- gitee: https://gitee.com/wallace89/myPyside6_QML/tree/main/thread02_safe_quit
一、main.py
python
# python3.10.11 - PySide6==6.9
import sys, time
from PySide6.QtCore import QObject, Signal, Slot, QThread
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
class Worker(QObject):
finished = Signal(str) # 完成信号
progress = Signal(int) # 进度信号
def __init__(self):
super().__init__()
self._is_running = True
def run(self):
self._is_running = True
for i in range(1, 11): # 模拟10步任务
if not self._is_running:
print("[Worker] 收到停止信号,安全退出")
self.finished.emit("任务已被取消")
return
print(f"[Worker] 工作中 {i}/10 ...")
self.progress.emit(i * 10) # 每次发进度
time.sleep(1)
self.finished.emit("任务完成!")
def stop(self):
self._is_running = False
class Backend(QObject):
resultReady = Signal(str)
progressChanged = Signal(int)
def __init__(self):
super().__init__()
self.thread = None
self.worker = None
self.is_task_running = False # 添加任务运行状态标志
def cleanup(self):
if not self.thread:
return # 没有线程,无需清理
# 1. 先停止线程的事件循环
self.thread.quit()
# 2. 等待线程完全退出
self.thread.wait()
# 3. 使用deleteLater()延迟删除对象(更安全)
self.worker.deleteLater()
self.thread.deleteLater()
print("[Backend] 已安排延迟清理 worker 和 thread")
self.thread = None
self.worker = None
@Slot()
def startTask(self):
# 检查是否已有任务在运行
if self.is_task_running:
print("[Backend] 任务已在运行中")
return
print("[Backend] 准备启动新任务")
# 创建新的线程和工作对象
self.thread = QThread()
self.worker = Worker()
self.worker.moveToThread(self.thread)
# 设置任务运行状态
self.is_task_running = True
# 线程启动
self.thread.started.connect(self.worker.run)
# worker 信号 → UI
self.worker.finished.connect(self.resultReady.emit)
self.worker.progress.connect(self.progressChanged.emit)
# 生命周期管理
self.worker.finished.connect(self._on_task_finished)
self.thread.start()
print("[Backend] 子线程已启动")
@Slot()
def stopTask(self):
if self.worker and self.is_task_running:
self.worker.stop()
print("[Backend] 停止任务请求已发出")
else:
print("[Backend] 没有运行中的任务可停止")
@Slot()
def _on_task_finished(self, message):
"""任务完成后的清理工作"""
print(f"[Backend] 任务完成: {message}")
self.is_task_running = False
self.cleanup() # 清理线程和worker
def clean_up_on_exit(self):
"""应用退出时的清理工作(处理用户中途退出的情况)"""
if self.is_task_running:
print("[Backend] 应用即将退出,停止运行中的任务")
self.worker.stop() # 停止任务
self.cleanup() # 清理线程资源
if __name__ == "__main__":
# 创建应用程序和引擎
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
# qml与python交互
backend = Backend() # 实例化python后端对象
engine.rootContext().setContextProperty("backend", backend) # 注册到QML环境(名叫 "backend")
# 连接应用程序退出信号,确保线程安全退出
app.aboutToQuit.connect(backend.clean_up_on_exit)
# 加载QML文件
engine.addImportPath(sys.path[0]) # 当前项目路径
engine.loadFromModule("Example", "Main") # 模块(Example) + QML文件名(Main.qml)
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
1.1、关键点1:Worker独立于线程
1.1.1、Worker ≠ 线程
"Worker独立于线程"是理解QThread工作机制的关键之一。 在Pyside6的多线程模型中,线程(QThtread)与工作对象(Worker)是两个完全独立的概念。理解这一点,是掌握安全退出与信号通讯的基础。
当我们写下:
python
self.thread = QThread()
self.worker = Worker()
self.worker.moveToThread(self.thread)
很多人误认为:
"
Worker现在就在子线程中运行。"
实际上,此时Worker 只是被"托管"给这个线程。 它的代码仍然在主线程中创建,只是未来当它接收到信号(例如 thread.started)时,那些槽函数才会在子线程的上下文中被调用。
也就是说:
QThread 提供的是执行环境,Worker 才是实际执行任务的对象,二者并非继承或包含关系,而是协作关系。
1.1.2、为什么要分离
如果让 Worker 自己继承 QThread(早期 Qt 写法),那么它的业务逻辑和线程管理会混在一起,比如:
python
class Worker(QThread):
def run(self):
...
这样虽然能用,但会带来两个问题:
- 线程和任务强绑定:Worker 只能运行一次,无法复用。
- 资源不好清理:run() 结束就直接退出线程,无法插入中间控制逻辑。
Qt 官方在 5.x 以后推荐的写法是:
"让 Worker 继承 QObject,由 QThread 管理它的运行环境。"
这种分离设计能让我们:
- 在主线程中安全创建和销毁对象;
- 在子线程中执行耗时逻辑;
- 在任务结束后平滑退出线程;
- 重用同一线程运行不同 Worker。
1.1.3、信号跨线程是关键
当执行:
python
self.thread.started.connect(self.worker.run)
时,Qt 自动根据线程归属创建一个跨线程的信号连接(Queued Connection)。
这意味着:
- 信号在主线程发出;
- 槽函数 run() 在 Worker 所属的线程(也就是子线程)执行;
- Qt 自动完成线程间事件传递,开发者无需手动加锁。
这一机制正是"Worker 独立于线程"设计的核心价值。
1.1.4、总结
Worker 独立于线程,意味着:
- Worker 是一个普通 QObject;
- QThread 只是它运行的宿主环境;
- 两者通过信号槽机制协作,而不是继承关系。
这种设计让我们能够在主线程中安全管理对象、在子线程中执行任务,并通过信号实现线程间通信------既灵活又安全。
1.2、关键点2:线程安全退出机制
在多线程编程中,"启动线程"并不难,真正的难点在于------让线程安全、优雅地退出。
如果处理不当,就会出现:
- 程序无法关闭(线程卡死);
- 崩溃(线程强行终止时破坏内存);
- 内存泄漏(线程对象未被销毁)。
1.2.1、为什么不能直接杀掉线程?
很多初学者看到 QThread 有一个 terminate() 方法,会尝试这样写:
python
self.thread.terminate()
然而,这种做法非常危险。它会让线程在任意位置被强制终止,此时:
- 对象析构函数可能还没执行;
- 文件句柄或串口未关闭;
- Qt 内部的锁、信号队列可能被破坏。
结果就是 "崩溃、死锁、资源泄露"。
所以,Qt 官方明确指出:
terminate() 仅用于紧急情况下,不推荐在正常业务中使用。
1.2.2、正确的思路:让线程"自己结束"
在安全退出机制中,主线程只负责发出停止信号,而子线程中的 Worker 要主动检测并自行退出。
python
class Worker(QObject):
finished = Signal(str) # 完成信号
progress = Signal(int) # 进度信号
def __init__(self):
super().__init__()
self._is_running = True
def run(self):
self._is_running = True
for i in range(1, 11): # 模拟10步任务
if not self._is_running:
print("[Worker] 收到停止信号,安全退出")
self.finished.emit("任务已被取消")
return
print(f"[Worker] 工作中 {i}/10 ...")
self.progress.emit(i * 10) # 每次发进度
time.sleep(1)
self.finished.emit("任务完成!")
def stop(self):
self._is_running = False
这样,调用:
python
self.worker.stop()
并不会立即终止线程,而是设置_is_running = False。Worker 在下一次循环检查到这个标志时,会安全地退出 run() 函数。
1.2.3、退出流程的流程
| 步骤 | 触发信号 | 执行对象 | 作用 |
|---|---|---|---|
| ① 用户点击"停止任务" | 调用 worker.stop() |
主线程 | 通知子线程:准备退出 |
② Worker 检测 _is_running=False |
self.finished.emit() |
子线程 | 告诉主线程:任务结束 |
③ Backend 收到 Worker.finished |
_on_task_finished() |
主线程 | 调用 cleanup() 执行资源清理 |
| ④ 清理函数中 | thread.quit() → thread.wait() |
主线程 | 退出事件循环,等待线程结束 |
| ⑤ 线程结束 | deleteLater() |
主线程 | 延迟销毁对象,彻底释放资源 |
1.2.4、deleteLater()的角色
deleteLater() 是 Qt 的安全销毁机制,它不会立即销毁对象,而是将"删除事件"投递给对象所在的事件循环。
这样可以避免:
- 对象仍在执行槽函数时被销毁;
- 槽函数结束后访问无效内存。
在本例中,我们在清理阶段执行:
python
self.worker.deleteLater()
self.thread.deleteLater()
由 Qt 自行决定安全的销毁时机,保证不崩溃、不泄漏。
1.2.5、总结
线程安全退出机制 = "让线程自己退出" + "主线程等待退出完成" + "Qt 事件循环负责清理"。
换句话说:
- 不强行终止;
- 不在子线程中直接删除对象;
- 所有销毁都交给 Qt 的事件循环来安全完成。
这是 Qt 多线程编程的黄金法则,也是实现高可靠后台任务的基础。
1.3、关键点3:Backend 类------线程生命周期管理
在前两个关键点中,我们理解了:
- Worker 独立于线程(谁执行、谁托管);
- 线程安全退出机制(谁发出、谁退出)。
接下来,主角登场------Backend 类。它相当于一个"线程调度中心",负责管理 QThread 与 Worker 的创建、启动、通信、退出 与清理全过程。
1.3.1、Backend 的职责是什么?
在 Qt 的多线程架构中,Backend 处于主线程中,
它的核心职责可以概括为 4 点:
| 职责 | 说明 |
|---|---|
| 1️创建与启动 | 创建 QThread 与 Worker,并建立信号槽连接 |
| 2️控制与通信 | 接收 QML 指令,发射信号给 Worker |
| 3️停止与退出 | 调用 worker.stop(),并在任务完成后清理线程 |
| 4️资源回收 | 使用 deleteLater() 与 wait() 安全释放资源 |
简单来说:
Backend 是桥梁,负责让主线程、子线程、UI 三方有序协作。
1.3.2、Backend 生命周期流程
当用户点击"开始任务"到"任务结束",
Backend 会依次经历以下五个阶段:
| 阶段 | Backend 行为 | 线程状态 | 描述 |
|---|---|---|---|
| 1️初始化 | 创建 QThread 与 Worker |
未启动 | 准备运行环境 |
| 2️启动任务 | 调用 thread.start() |
运行中 | Worker.run() 在子线程中执行 |
| 3️停止任务 | 调用 worker.stop() |
正在退出 | Worker 检测标志后主动退出 |
| 4️任务结束 | 收到 worker.finished |
正在清理 | 调用 cleanup() 安全退出 |
| 5️资源回收 | 调用 deleteLater() |
已销毁 | 线程与 Worker 对象释放内存 |
1.3.3、核心方法解析
startTask()--- 启动线程
python
def startTask(self):
self.thread = QThread()
self.worker = Worker()
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.worker.finished.connect(self._on_task_finished)
self.thread.start()
- 创建执行环境(QThread)与任务对象(Worker);
- 通过 moveToThread() 建立归属关系;
- 信号槽连接确保 run() 在子线程中执行。
stopTask()--- 请求停止
python
def stopTask(self):
if self.worker and self.is_task_running:
self.worker.stop()
- 主线程调用 stop(),
- Worker 检测
_is_running = False,自行退出循环。
这一过程是非阻塞的,主线程不会被卡住。
_on_task_finished()--- 接收完成信号
python
def _on_task_finished(self, message):
self.is_task_running = False
self.cleanup()
- 当任务结束,Worker.finished 信号触发;
- Backend 接收结果并调用 cleanup() 统一清理。
cleanup()--- 资源安全回收
python
def cleanup(self):
self.thread.quit()
self.thread.wait()
self.worker.deleteLater()
self.thread.deleteLater()
- quit() → 停止线程事件循环;
- wait() → 等待线程完全退出;
- deleteLater() → 延迟销毁对象,防止访问已删除的内存。
1.3.4、总结
Backend 是整个多线程系统的"大脑",负责统筹 Worker 的任务执行与 QThread 的生命周期管理。它确保线程能正确启动、可控运行、优雅退出、彻底清理,让 UI 始终保持响应而程序稳定运行。
1.4、关键点4:信号连接顺序
在使用 QThread 管理子线程时,很多人都会遇到这样的疑问:
- "为什么我一启动线程就崩溃?"
- "为什么信号没触发?"
- "为什么线程结束后对象没销毁?"
这些问题几乎都与------信号连接的顺序 有关。
Qt 的信号槽机制是事件驱动的,而事件循环又依赖于线程状态。
因此,信号的连接顺序决定了线程能否被正确启动、执行和退出。
1.4.1、信号连接顺序的黄金法则
一定要在 thread.start()之前完成所有信号连接。
在 Qt 中,当你调用 thread.start() 时,线程就会立即进入事件循环(QThread::run() → exec())。如果这时还没建立信号槽连接,就会出现:
- 信号触发时找不到槽函数;
- 线程启动后立刻退出;
- 对象提前被销毁,导致 "Internal C++ object already deleted" 错误。
1.4.2、正确的连接顺序
| 步骤 | 代码 | 说明 |
|---|---|---|
| 1. 创建线程和 Worker | self.thread = QThread() self.worker = Worker() |
先创建对象 |
| 2. 建立归属关系 | self.worker.moveToThread(self.thread) |
让 Worker 在子线程运行 |
| 3. 连接信号槽 | 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) |
在启动前建立所有连接 |
| 4. 启动线程 | self.thread.start() |
最后一步,进入事件循环 |
| 这四个阶段缺一不可,也必须按顺序执行。信号在前,启动在后是 Qt 的基本线程准则。 |
二、Main.qml
js
// 导入QtQuick模块,提供基本的QML元素
import QtQuick
// 导入QtQuick.Controls模块,提供UI控件如Button
import QtQuick.Controls
// 定义一个窗口组件,作为应用程序的主窗口
Window {
width: 350
height: 280
visible: true
title: "QThread 生命周期与安全退出 - 改进版"
// 使用Column布局垂直排列UI元素
Column {
anchors.centerIn: parent
spacing: 20
// 按钮行,包含开始和停止按钮
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 15
// 开始任务按钮
Button {
text: "开始任务"
onClicked: {
backend.startTask()
// 重置进度条和状态文本
progressBar.value = 0
if (resultText.text === "等待任务..." ||
resultText.text === "任务完成!" ||
resultText.text === "任务已被取消") {
resultText.text = "任务启动中..."
}
}
}
// 停止任务按钮
Button {
text: "停止任务"
onClicked: backend.stopTask()
}
}
// 进度条显示任务进度
ProgressBar {
id: progressBar
width: 250
anchors.horizontalCenter: parent.horizontalCenter
value: 0
// 添加进度百分比文本
Text {
anchors.centerIn: parent
text: Math.round(progressBar.value * 100) + "%"
font.pointSize: 10
color: "white"
}
}
// 结果文本显示任务状态和结果
Text {
id: resultText
text: "等待任务..."
font.pointSize: 16
anchors.horizontalCenter: parent.horizontalCenter
}
// 说明文本
Text {
text: "点击'开始任务'可重复启动新任务"
font.pointSize: 10
color: "gray"
anchors.horizontalCenter: parent.horizontalCenter
}
}
// 信号连接,处理后端发来的信号
Connections {
target: backend
// 处理进度更新信号
function onProgressChanged(val) {
progressBar.value = val / 100.0
}
// 处理任务结果信号
function onResultReady(msg) {
resultText.text = msg
}
}
}
三、细节补充
3.1、QObject.deleteLater():线程中对象的安全销毁机制

既然worker并不是QThread,为什么在finished信号发出时,仍然要调用worker.deleteLater()方法销毁worker?因为worker的父类是QObject,一个良好的 Qt 编程习惯:让 Qt 在事件循环安全时机删除对象,避免潜在的多线程访问问题。尤其在复杂工程中,这行代码能有效预防悬空对象崩溃。
我尝试将代码self.worker.deleteLater()注释掉,程序依然能正常运行。为什么?即使不调用 deleteLater(),Python 的垃圾回收机制(引用计数归零)也会释放对象。因此,表面上看不出任何内存泄漏或崩溃。但是,让Python的垃圾回收机制来释放worker对象并不是编写Qt程序的好习惯。 因为,直接让 Python 回收这个对象(通过 self.worker = None),C++ 层的 QObject 可能还在被其他线程访问(尤其是跨线程信号传输的场景),就可能在特定时机引发崩溃。
总之,只要继承自QObject类的对象在实例化并使用完毕后要销毁的话,一定要调用deleteLater()来延迟销毁。