【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(),那么主线程会停在这里,直到 myThreadrun() 函数返回,主线程才会继续执行。

    • 参数是超时时间(毫秒)。默认是 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博客

相关推荐
egoist20232 小时前
[linux仓库]图解System V共享内存:从shmget到内存映射的完整指南
linux·开发语言·共享内存·system v
兰亭妙微3 小时前
兰亭妙微QT软件开发与UI设计协同:如何避免设计与实现脱节?
开发语言·qt·ui
深思慎考3 小时前
【新版】Elasticsearch 8.15.2 完整安装流程(Linux国内镜像提速版)
java·linux·c++·elasticsearch·jenkins·框架
今天头发还在吗4 小时前
【Docker】在项目中如何实现Dockerfile 文件编写
java·docker·容器
1710orange4 小时前
java设计模式:动态代理
java·开发语言·设计模式
开心-开心急了4 小时前
PySide6 文本编辑器(QPlainTextEdit)实现查找功能——重构版本
开发语言·python·ui·重构·pyqt
郝学胜-神的一滴4 小时前
Effective Python 第39条:通过@classmethod多态来构造同一体系中的各类对象
开发语言·python·程序人生·软件工程
ajassi20004 小时前
开源 C++ QT QML 开发(四)复杂控件--Listview
c++·qt·开源
聪明的笨猪猪5 小时前
Java “并发工具类”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试