当前位置: 首页 > news >正文

【Qt】多线程

目录

一.QThread的介绍和使用

1.1.常用API

1.2.finished() - 线程结束信号

1.3.简单示例

二.线程安全

2.1.QMutex

2.2.QMutexLocker

2.3.QReadWriteLocker、QReadLocker、QWriteLocker

2.4.条件变量

2.5.信号量


一.QThread的介绍和使用

事实上啊,Qt中的多线程和Linux的多线程是差不多的!!!

在Qt中,多线程的处理⼀般是通过QThread类来实现。

QThread代表⼀个在应⽤程序中可以独⽴控制的线程,也可以和进程中的其他线程共享数据。

QThread对象管理程序中的⼀个控制线程。

1.1.常用API

1. run() - 线程的⼊⼝函数

  • 核心作用:这是线程的“主函数”。一个线程的生命就是从 run() 函数的开始到结束。

  • 工作方式

    • 当你创建一个继承自 QThread 的子类时,你需要重写这个 run() 方法。

    • 在这个重写的 run() 函数里,你放置所有需要在新线程中执行的代码。

    • 当 run() 函数执行完毕(执行到末尾或遇到 return),线程就自然结束了。

  • 重要警告

    • 你永远不应该直接调用 run()启动线程应该使用 start() 方法。如果你直接调用 run(),它就像一个普通的函数调用一样,会在当前线程中执行,而不会创建新的执行线程。


2. start() - 启动线程

  • 核心作用:通知操作系统启动一个新的线程,并由操作系统调度该线程开始执行其 run() 函数。

  • 工作方式

    • 调用 start() 后,线程进入“就绪”状态。操作系统会根据其优先级和系统负载,在某个时刻真正开始执行 run() 函数。这有一个微小的延迟。

    • 如果线程已经在运行,再次调用 start() 是无效的,它什么也不会做。

  • 与 run() 的关系start()是正确启动线程的唯一方式。start() -> (操作系统调度) -> run()


3. currentThread() - 获取当前线程对象

  • 核心作用:一个静态函数,返回一个指向管理当前执行线程的 QThread 指针。

  • 工作方式

    • 它告诉你“当前这行代码是在哪个线程里运行的”。

    • 这对于调试、日志记录以及在需要根据所在线程做出不同行为的函数中非常有用。


4. isRunning() - 检查线程状态

  • 核心作用:查询线程是否正在活跃地执行。即,start() 已被调用,且 run() 函数还未返回。

  • 工作方式

    • 返回 true:线程已启动且尚未结束。

    • 返回 false:线程还未启动 (start() 未被调用),或者已经正常结束/被终止。

  • 用途:通常用于条件判断,比如在关闭程序时,检查是否有工作线程还在运行。


5. sleep() / msleep() / usleep() - 线程休眠

  • 核心作用:强制让当前线程暂停执行一段指定的时间。

  • 区别

    • sleep(long secs):休眠,单位是

    • msleep(long msecs):休眠,单位是毫秒

    • usleep(long usecs):休眠,单位是微秒

  • 重要警告

    • 这些是静态函数,它们让调用它们的当前线程休眠。

    • 它们会阻塞当前线程。在 GUI 线程(主线程)中调用它们会导致界面卡死。

    • Qt 官方文档已不建议使用这些函数,因为它们不优雅且容易被误用。推荐使用 QTimer 或事件循环来实现非阻塞的延迟。

6. wait() - 等待线程结束


  • 核心作用阻塞调用它的线程,直到目标线程执行完毕或等待超时。

  • 工作方式

    • 它像一个“汇合点”。比如,在主线程中调用 myThread->wait(),那么主线程会停在这里,直到 myThread 的 run() 函数返回,主线程才会继续执行。

    • 参数是超时时间(毫秒)。默认是 ULONG_MAX,意味着无限等待。

    • 如果线程成功结束,返回 true;如果是因为超时而返回,则返回 false

  • 典型用途

    • 在程序退出时,确保所有工作线程都已完成它们的任务,再进行清理。

    • 类似于 POSIX 的 pthread_join,用于回收线程资源。


7. terminate() - 强制终止线程

  • 核心作用:立即、强制地停止线程的执行。

  • 工作方式

    • 它告诉操作系统立即终止目标线程,不管线程执行到哪一步。

    • 这是极其危险的! 线程可能在修改数据结构、持有锁、或正在分配/释放内存时被突然终止,导致数据损坏、死锁或内存泄漏。

    • Qt 官方强烈不建议使用此函数,除非在万不得已的情况下(例如,线程陷入死循环且无法通过其他方式退出)。


1.2.finished() - 线程结束信号

  • 核心作用:这是一个信号当线程的 run() 函数执行完毕时,QThread 对象会自动发出这个信号。

  • 工作方式

    • 利用 Qt 的信号槽机制,你可以将这个信号连接到一个槽函数上。

    • 当线程结束时,这个槽函数就会被调用。

  • 主要用途

    • 资源清理:最经典的用法是 connect(thread, &QThread::finished, thread, &QObject::deleteLater)。这样,当线程结束后,线程对象自身会被安全地删除。

    • 状态通知:通知其他部分(如主线程的GUI),工作已经完成,可以更新界面或进行下一步操作。

    • 启动链式任务:一个线程结束时,触发另一个线程启动。


1.3.简单示例

创建线程的步骤如下:

  1. 自定义线程类
    首先,创建一个自定义类,继承自 QThread。在该类中,必须重写父类的 run() 函数。这个 run() 函数即为线程处理函数,其内部代码将在新的子线程中执行,与主线程相互独立。

  2. 实现线程处理逻辑
    在 run() 函数中编写这个线程需要执行的任务的代码。

  3. 正确启动线程
    启动线程时,不应直接调用 run() 方法,而应通过线程对象调用 start() 方法。start() 会内部调用 run(),并确保其运行在新建的线程中。

  4. 线程结束信号通知
    可以在自定义线程类中定义信号(如 finished 或自定义信号),并在 run() 函数执行结束时发射该信号。这样,主线程可以通过连接该信号,执行相应的清理工作或状态更新。

  5. 安全关闭线程
    线程执行完毕后,应适当释放资源并安全退出。可以使用 quit() 和 wait() 方法确保线程正确结束,避免资源泄漏。若需强制结束,可使用 terminate(),但建议仅在必要时使用,以确保程序稳定性。

通过以上步骤,能够有效地在 Qt 中使用多线程,提升程序的并发处理能力与用户体验。


我们创建一个项目

我们就使用多线程来实现一下定时器这个功能

创建另外一个线程,在新线程中实现计时。

我们需要创建一个类,继承自QThread

我们去看看

我们发现还是老问题啊,我们需要添加一下头文件啊

我们继承QThread的目的是为了重写run()

注意:我们不能在新线程来对界面进行任何修改!!

现在我们就回到我们的主程序

我们运行一下

……

……

也是实现了啊!!!

二.线程安全

2.1.QMutex

我们直接写个例子

我们先创建一个项目

我们需要创建一个类,继承自QThread

我们去看看

我们发现还是老问题啊,我们需要添加一下头文件啊

我们继承QThread的目的是为了重写run()

我们运行一下

很明显这个值不是1000000啊!!那么我们怎么进行处理呢?

我们就需要进行加锁啊

我们这次再运行一下

这次就没有问题了吧!!


2.2.QMutexLocker

在实际开发中,使用锁(lock)保护临界区时,涉及的代码逻辑往往较为复杂,很容易在某个条件分支或异常处理中遗漏解锁(unlock)操作,从而导致死锁等问题。

类似的问题也出现在动态内存管理上——如果在释放内存之前提前返回或抛出异常,就容易造成内存泄漏。

C++ 通过引入“智能指针”(smart pointer)来自动管理内存资源的释放,有效避免了上述问题。同样地,为了自动化地管理互斥量的加锁与解锁,C++11 引入了 std::lock_guard 类模板。它基于 RAII(Resource Acquisition Is Initialization)机制,在构造时锁定互斥量,在析构时自动解锁,从而保证即使发生异常也能安全释放锁。

其基本用法如下:

{std::lock_guard<std::mutex> guard(mutex);// 执行受保护的复杂逻辑// ...
} // 离开作用域时,guard 析构,自动调用 unlock

Qt 框架也借鉴了相同的设计思想,提供了 QMutexLocker 类,实现类似的功能:

{QMutexLocker locker(&mutex);// 受保护的代码段// ...
} // locker 析构时自动解锁

这种 RAII 风格的管理机制,极大地提升了代码的可靠性和可维护性,是现代 C++ 和 Qt 开发中推荐的做法。


这个的功能和上面那个加锁的是一模一样的。

得到的结果也是下面这个

这个不会出现忘记解锁的情况。

注意:

在编写多线程程序时,既可以使用 Qt 提供的锁机制(如 QMutex),也可以使用 C++ 标准库中的锁(如 std::mutex)。这两种锁本质上都是对操作系统原生锁功能的封装,因此在功能上具有一定的互通性。

从原理上来说,C++ 标准库中的锁是可以用于同步 Qt 线程的。这是因为 Qt 的线程底层仍然基于系统的线程实现(例如 pthreads 或 Windows Threads),而 C++ 的锁也是构建在同一底层机制之上的,因此它们能够识别并作用于同一个线程系统中的线程。

尽管技术上可行,但在实际开发中一般不建议混合使用不同来源的锁机制。主要原因包括:

  • 一致性与可读性:统一使用同一套线程与锁机制(如全部使用 Qt 或全部使用 C++ 标准库)有助于保持代码风格一致,降低理解和维护成本。

  • 避免不必要的依赖:若在 Qt 项目中大量使用 C++ 标准库的锁,可能会增加代码的复杂度,尤其在已经依赖 Qt 线程模块的情况下。

  • 功能与集成度:Qt 的锁(如 QMutexLocker)与 Qt 框架的其他部分(如信号槽、事件循环)有更好的集成,使用 Qt 自带的锁能够更自然地与框架协作。

因此,虽然 C++ 标准库的锁能够用于 Qt 线程,但在 Qt 项目中,推荐优先使用 Qt 自身提供的线程同步工具,以保持架构上的一致性和框架优势。

2.3.QReadWriteLocker、QReadLocker、QWriteLocker

特点:

  • QReadWriteLock 是读写锁类,⽤于控制读和写的并发访问。
  • QReadLocker⽤于读操作上锁,允许多个线程同时读取共享资源。
  • QWriteLocker ⽤于写操作上锁,只允许⼀个线程写⼊共享资源。

⽤途:在某些情况下,多个线程可以同时读取共享数据,但只有⼀个线程能够进⾏写操作。读写锁提 供了更⾼效的并发访问⽅式。

// 创建一个读写锁对象,用于管理对共享资源的并发访问
QReadWriteLock rwLock;// 在读操作中使用读锁
{// 创建QReadLocker对象,在构造时自动获取读锁// 多个线程可以同时持有读锁,实现并发读取QReadLocker locker(&rwLock);// 在作用域内读取共享资源// 此时其他线程也可以同时读取,但不能写入// ...} // QReadLocker在作用域结束时自动释放读锁(析构函数中解锁)// 在写操作中使用写锁
{// 创建QWriteLocker对象,在构造时自动获取写锁// 写锁是排他性的,同一时间只能有一个线程持有写锁QWriteLocker locker(&rwLock);// 在作用域内修改共享资源// 此时其他线程既不能读取也不能写入,保证数据一致性// ...} // QWriteLocker在作用域结束时自动释放写锁(析构函数中解锁)

关键点说明:

  1. QReadWriteLock:提供读写锁机制,允许多个读者或一个写者访问共享资源

  2. QReadLocker

    • 构造时自动加读锁

    • 析构时自动解读锁

    • 支持多个线程同时持有读锁

  3. QWriteLocker

    • 构造时自动加写锁

    • 析构时自动解写锁

    • 写锁是排他性的,会阻塞所有其他读写操作

  4. RAII模式:利用对象的生命周期自动管理锁的获取和释放,避免忘记解锁导致的死锁

2.4.条件变量

注意:我们这里的条件变量和Linux上的是一模一样的。

在多线程编程中,假设除了等待操作系统正在执⾏的线程之外,某个线程还必须等待某些条件满⾜才 能执⾏,这时就会出现问题。

这种情况下,线程会很⾃然地使⽤锁的机制来阻塞其他线程,因为这只 是线程的轮流使⽤,并且该线程等待某些特定条件,⼈们会认为需要等待条件的线程,在释放互斥锁 或读写锁之后进⼊了睡眠状态,这样其他线程就可以继续运⾏。当条件满⾜时,等待条件的线程将被 另⼀个线程唤醒。

在Qt中,专⻔提供了QWaitCondition类来解决像上述这样的问题。

特点:QWaitCondition是Qt框架提供的条件变量类,⽤于线程之间的消息通信和同步。

⽤途:在某个条件满⾜时等待或唤醒线程,⽤于线程的同步和协调。


什么是 QWaitCondition?

你可以把 QWaitCondition 想象成一个线程间的“协调员”或“信号灯”。它的核心作用是:让一个线程在某些条件不满足时主动进入等待(休眠)状态,并在另一个线程使条件满足后,被唤醒并继续执行。

它解决了什么问题?在没有 QWaitCondition 的情况下,如果线程需要等待某个条件,它可能不得不使用“忙等待”(busy-waiting),即在一个循环里不停地检查条件,这非常消耗CPU资源。QWaitCondition 提供了一种高效的方式,让线程在等待时“睡觉”,不占用CPU,直到条件满足时才被唤醒。

核心要点: QWaitCondition 必须与一个 互斥锁(QMutex 或 QReadWriteLock) 配合使用。这个锁用于保护“条件”本身(即那个共享的、线程不安全的状态变量)。


常用接口详解

让我们来逐一剖析 QWaitCondition 的几个关键成员函数。

1. bool wait(QMutex *lockedMutex, QDeadlineTimer deadline = QDeadlineTimer(QDeadlineTimer::Forever))

(这是 Qt 5.15/6 推荐的带超时参数的新接口,比老式的 wait(mutex, unsigned long time) 更现代)

  • 功能:这是最核心的等待函数。调用它的线程会释放它已经锁定的 lockedMutex,然后使当前线程进入等待(阻塞)状态

    • 等待被唤醒:直到其他线程调用了 wakeOne() 或 wakeAll() 来唤醒它。

    • 等待超时:或者直到 deadline 指定的超时时间到达。

  • 参数

    • lockedMutex:一个已经被当前线程锁定的互斥锁的指针。这是关键!在调用 wait() 之前,你必须先 lock() 这个锁。

    • deadline:一个 QDeadlineTimer 对象,指定等待的最晚期限。默认是 Forever,意味着无限期等待。

  • 返回值

    • true:如果线程是被 wakeOne() 或 wakeAll() 唤醒的。

    • false:如果是因为超时而唤醒的。

  • 内部执行流程(非常重要!)

    1. 前提:当前线程已经成功锁定了 lockedMutex

    2. 调用 wait(...)

    3. 系统原子性地执行两个操作:
      a. 释放 lockedMutex(让其他线程可以获取它来修改条件)。
      b. 将当前线程挂起(进入睡眠)。

      • “原子性”意味着这两个操作不可分割,避免了竞争条件。

    4. 当线程被唤醒(或因超时醒来)时,函数在返回前,会重新尝试获取(锁定) lockedMutex。所以当 wait() 函数返回时,当前线程再次持有了这个锁。

  • 典型使用模式(伪代码)

    // 等待者线程 (Consumer)
    mutex.lock(); // 第一步:先上锁,保护下面的“条件检查”
    while (!conditionIsMet) { // 第二步:用循环检查条件是否满足(防止虚假唤醒)// 条件不满足,开始等待bool wokeBySignal = waitCondition.wait(&mutex, timeout);if (!wokeBySignal) {// 处理超时逻辑}// 如果被唤醒,会再次循环检查 conditionIsMet 是否真的为 true
    }
    // ... 条件满足了,做相应的工作 ...
    mutex.unlock(); // 第三步:工作做完后,解锁

2. void wakeOne()

  • 功能:唤醒一个正在该 QWaitCondition 上等待的线程。具体唤醒哪一个是不确定的,由操作系统的调度器决定。

  • 使用场景:当你知道条件满足后,只需要唤醒一个线程就足够时使用。例如,在生产者-消费者模型中,生产者只生产了一个数据项,只需要唤醒一个消费者来处理它。

  • 用法

    // 唤醒者线程 (Producer)
    mutex.lock();
    // ... 修改共享数据,使条件变为满足状态 ...
    conditionIsMet = true;
    waitCondition.wakeOne(); // 通知一个等待的线程
    mutex.unlock();

    注意:通常建议在持有互斥锁的情况下调用 wakeOne()。这样可以保证,在你修改条件和发送唤醒信号之间,不会有其他线程插足,避免了某些微妙的竞争条件。

3. void wakeAll()

  • 功能:唤醒所有正在该 QWaitCondition 上等待的线程。

  • 使用场景:当条件满足后,所有等待的线程都有可能继续工作时使用。例如:

    • 一个资源从“不可用”变为“可用”,所有等待该资源的线程都可以来竞争。

    • 你希望关闭程序,需要通知所有等待的工作线程退出。

  • 用法:与 wakeOne() 类似,只是调用的是 wakeAll()

    mutex.lock();
    // ... 修改共享数据 ...
    globalResourceAvailable = true;
    waitCondition.wakeAll(); // 通知所有等待的线程
    mutex.unlock();

总的来说,条件变量的使用就像是下面这样子

// 创建互斥锁和条件变量
QMutex mutex;
QWaitCondition condition;// 在等待线程中
mutex.lock();  // 获取互斥锁,保护共享资源的访问// 检查条件是否满足,使用while循环防止虚假唤醒
while (!conditionFullfilled()) 
{// 条件不满足时等待// wait()会暂时释放mutex并让线程进入等待状态// 当被唤醒时,它会重新获取mutex然后继续执行condition.wait(&mutex);  
}// 条件满足后继续执行相关操作
// ...
mutex.unlock();  // 释放互斥锁// 在改变条件的线程中
mutex.lock();  // 获取互斥锁,保护对共享条件的修改// 改变条件(通常是修改某些共享变量)
changeCondition();// 唤醒所有等待该条件的线程
// 这些线程会从condition.wait()中返回并重新检查条件
condition.wakeAll(); mutex.unlock();  // 释放互斥锁

我们这里不细讲这个条件变量,感兴趣的可以去:【Linux】多线程4——线程同步/条件变量_linux 线程同步-CSDN博客

2.5.信号量

有时在多线程编程中,需要确保多个线程可以相应的访问⼀个数量有限的相同资源。例如,运⾏程序 的设备可能是⾮常有限的内存,因此我们更希望需要⼤量内存的线程将这⼀事实考虑在内,并根据可 ⽤的内存数量进⾏相关操作,多线程编程中类似问题通常⽤信号量来处理。

信号量类似于增强的互斥 锁,不仅能完成上锁和解锁操作,⽽且可以跟踪可⽤资源的数量。

特点:QSemaphore是Qt框架提供的计数信号量类,⽤于控制同时访问共享资源的线程数量。

⽤途:限制并发线程数量,⽤于解决⼀些资源有限的问题。

QSemaphore semaphore(2); //同时允许两个线程访问共享资源//在需要访问共享资源的线程中semaphore.acquire(); //尝试获取信号量,若已满则阻塞//访问共享资源//...semaphore.release(); //释放信号量//在另⼀个线程中进⾏类似操作

我们这里不细讲这个信号量,感兴趣的可以去:【Linux】多线程6——POSIX信号量,环形队列cp问题_posixpv操作-CSDN博客

http://www.dtcms.com/a/445732.html

相关文章:

  • 如何把qt + opencv的库按需要拷贝到开发板
  • 网络安全设备 防火墙
  • Java学习之旅第二季-6:static关键字与this关键字
  • 高校健康驿站建设指引妖精直播
  • 违规通知功能修改说明
  • SOFA 架构--01--简介
  • 家具网站首页模板郑州销售网站
  • 如何将Spring Boot 2接口改造为MCP服务,供大模型调用!
  • DC-DC电源芯片解读:RK860
  • 从零开始的C++学习生活 3:类和对象(中)
  • 做网站的技术员包装设计概念
  • 【深度学习02】TensorBoard 基础与 torchvision 图像变换工具详解(附代码演示)
  • k8s中Pod和Node的故事(1):过滤、打分、亲和性和拓扑分布
  • springboot自助甜品网站的设计与实现(代码+数据库+LW)
  • 网站建设业动态wordpress出现404
  • Vue3组件通信8大方式详解
  • LeetCode 刷题【100. 相同的树、101. 对称二叉树、102. 二叉树的层序遍历】
  • Go基础:Go语言应用的各种部署
  • 团购网站 seo电商网站怎么做
  • 无Dockerfile构建:云原生部署新姿势
  • 深入解析 IDM 插件开发挑战赛:技术要点与实践指南
  • 颜群JVM【03】类的初始化
  • 达梦数据库常用初始化参数与客户端工具使用
  • 命令行安装 MySQL 8.4.6
  • 数据库--数据库约束和表的设计
  • [Windows] 磁盘映像管理工具:WimTool v1.7.2025.1001
  • 公司自己做网站晋城企业网站建设价格
  • 【SpringCloud(1)】初识微服务架构:创建一个简单的微服务;java与Spring与微服务;初入RestTemplate
  • leetcode 79 单词搜索
  • 站长之家官网查询电子商务网站建设与实践上机指导