【Qt】多线程

目录

一.QThread的介绍和使用

1.1.常用API

[1.2.finished() - 线程结束信号](#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(),它就像一个普通的函数调用一样,会在当前线程中执行,而不会创建新的执行线程。

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

  • 工作方式:

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

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

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


  1. currentThread() - 获取当前线程对象
  • 核心作用 :一个静态函数,返回一个指向管理当前执行线程 的 QThread 指针。

  • 工作方式:

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

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


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

  • 工作方式:

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

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

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


  1. sleep() / msleep() / usleep() - 线程休眠
  • 核心作用 :强制让当前线程暂停执行一段指定的时间。

  • 区别:

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

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

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

  • 重要警告:

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

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

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

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

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

  • 工作方式:

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

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

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

  • 典型用途:

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

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


  1. 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)机制,在构造时锁定互斥量,在析构时自动解锁,从而保证即使发生异常也能安全释放锁。

其基本用法如下:

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

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

cpp 复制代码
{
    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 ⽤于写操作上锁,只允许⼀个线程写⼊共享资源。

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

cpp 复制代码
// 创建一个读写锁对象,用于管理对共享资源的并发访问
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() 函数返回时,当前线程再次持有了这个锁。

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

    cpp 复制代码
    // 等待者线程 (Consumer)
    mutex.lock(); // 第一步:先上锁,保护下面的"条件检查"
    while (!conditionIsMet) { // 第二步:用循环检查条件是否满足(防止虚假唤醒)
        // 条件不满足,开始等待
        bool wokeBySignal = waitCondition.wait(&mutex, timeout);
        if (!wokeBySignal) {
            // 处理超时逻辑
        }
        // 如果被唤醒,会再次循环检查 conditionIsMet 是否真的为 true
    }
    // ... 条件满足了,做相应的工作 ...
    mutex.unlock(); // 第三步:工作做完后,解锁
  1. void wakeOne()
  • 功能 :唤醒一个正在该 QWaitCondition 上等待的线程。具体唤醒哪一个是不确定的,由操作系统的调度器决定。

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

  • 用法:

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

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

  1. void wakeAll()
  • 功能 :唤醒所有正在该 QWaitCondition 上等待的线程。

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

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

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

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

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

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

cpp 复制代码
// 创建互斥锁和条件变量
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框架提供的计数信号量类,⽤于控制同时访问共享资源的线程数量。

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

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

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

相关推荐
阿俊-全栈开发3 小时前
LikeShop 单商户SaaS商城分销模块二开:佣金计算与结算逻辑实现
java·开发语言·微服务·likeshop·likeshop开源商城·likeshop多商户
wuminyu6 小时前
C++协程实现接收端的零拷贝Buffer管理原理剖析
java·linux·c语言·jvm·c++
m0_380743877 小时前
C++中的freopen的用法实例详解
开发语言·c++
Escalating_xu7 小时前
【Python】基础语法(1):常量、变量、类型、输入输出与运算符
开发语言·python
jimy17 小时前
polymorph.cpp里面的“析构函数、智能指针”问题
开发语言·c++
m0_380743877 小时前
PHP 8.3 缓存清除不生效怎么排查
开发语言·php
朝朝辞暮i8 小时前
C++ 第 6 课:switch —— 多状态选择
开发语言·c++·算法
小凡geo8 小时前
本地商家 GEO:用脚本一键生成 FAQPage 结构化数据,让 AI 切片更稳
开发语言·人工智能·python·microsoft·搜索引擎·ai
朝朝辞暮i8 小时前
C++ 第 4 课:if / else —— 让程序自己做决定
开发语言·c++
波力海苔夹心脆6759 小时前
C# 值类型与引用类型详解:存储位置、赋值机制、参数传递、相等比较、装箱拆箱与常见陷阱
开发语言·jvm·经验分享·笔记·c#·.net