PySide6 + QML - 多线程02 - QThread 生命周期与安全退出

线程不仅要能启动,还要能正确退出。 否则容易出现 内存泄漏、僵尸线程、甚至程序崩溃。

在上一章节《01:QThread 基础》中,我们学习了如何用 Worker + QThread 的模式把耗时任务放到子线程执行,并通过信号把结果传回 UI,从而保证界面不卡顿。那一章的重点是 "能跑起来"

但是,线程的生命周期不仅仅是"启动和执行"。如果我们只考虑启动,而忽略了安全退出,就可能出现以下问题:

  • 任务中途退出困难:比如用户点击"停止"按钮,但子线程仍在继续工作。
  • 资源无法释放:线程对象或 Worker 对象没有被销毁,导致内存泄漏。
  • 强行终止风险:直接调用 terminate() 会让线程在任意位置被中断,可能破坏内存或数据。

因此,本章将重点讲解QThread 的生命周期与安全退出。 我们会在上一章的基础上,增加可控的停止机制 ,让子线程能在任务中途被优雅地结束,并且在退出时正确释放资源。

如上所示:

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

效果如下所示:

工程代码:

一、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):
        ...

这样虽然能用,但会带来两个问题:

  1. 线程和任务强绑定:Worker 只能运行一次,无法复用。
  2. 资源不好清理: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️创建与启动 创建 QThreadWorker,并建立信号槽连接
2️控制与通信 接收 QML 指令,发射信号给 Worker
3️停止与退出 调用 worker.stop(),并在任务完成后清理线程
4️资源回收 使用 deleteLater()wait() 安全释放资源

简单来说:

Backend 是桥梁,负责让主线程、子线程、UI 三方有序协作。

1.3.2、Backend 生命周期流程

当用户点击"开始任务"到"任务结束",

Backend 会依次经历以下五个阶段:

阶段 Backend 行为 线程状态 描述
1️初始化 创建 QThreadWorker 未启动 准备运行环境
2️启动任务 调用 thread.start() 运行中 Worker.run() 在子线程中执行
3️停止任务 调用 worker.stop() 正在退出 Worker 检测标志后主动退出
4️任务结束 收到 worker.finished 正在清理 调用 cleanup() 安全退出
5️资源回收 调用 deleteLater() 已销毁 线程与 Worker 对象释放内存

1.3.3、核心方法解析

  1. 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() 在子线程中执行。
  1. stopTask() --- 请求停止
python 复制代码
def stopTask(self):
    if self.worker and self.is_task_running:
        self.worker.stop()
  • 主线程调用 stop(),
  • Worker 检测 _is_running = False,自行退出循环。
    这一过程是非阻塞的,主线程不会被卡住。
  1. _on_task_finished() --- 接收完成信号
python 复制代码
def _on_task_finished(self, message):
    self.is_task_running = False
    self.cleanup()
  • 当任务结束,Worker.finished 信号触发;
  • Backend 接收结果并调用 cleanup() 统一清理。
  1. 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()来延迟销毁。

相关推荐
Eiceblue6 小时前
Python 快速提取扫描件 PDF 中的文本:OCR 实操教程
vscode·python·ocr·1024程序员节
coding消烦员18 小时前
新版 vscode 去除快捷键 Ctrl+I 显示 Copilot 的 AI 对话框
人工智能·vscode·copilot
农场主John1 天前
vscode断点使用
ide·vscode·编辑器
Caesar Zou1 天前
解决 Codex 在 WSL/SSH/VSCODE 登录时报 “Token exchange failed: 403 Forbidden” 问题
ide·vscode·编辑器
Dobby_051 天前
【Go】C++ 转 Go 第(四)天:结构体、接口、反射、标签 | 面向对象编程
vscode·golang·1024程序员节
江公望1 天前
如何在Qt QML中定义枚举浅谈
开发语言·qt·qml
小二·2 天前
Visual Studio Code 高效开发完全指南(2025年更新版)
ide·vscode·编辑器
Hi202402172 天前
Qt+Qml客户端和Python服务端的网络通信原型
开发语言·python·qt·ui·网络通信·qml
我狸才不是赔钱货2 天前
VsCode + Wsl:终极开发环境搭建指南
ide·vscode·编辑器