【C++ Qt 】中的多线程QThread已经线程安全相关的锁QMutex、QMutexLocker


每日激励:"不设限和自我肯定的心态:I can do all things。 --- Stephen Curry"
**绪论:
好久没更新Qt内容了,这个也是2026新年第一篇,最近再捡起Qt的内容也在简单的复习,所以就出了这篇博客它其实也是很久前面写好的草稿一直没时间整理一遍,现在整理好了这篇博客是入门Qt多线程在这基础上希望你提前对多线程有至少简单的理解(完全没意识可以问问AI哦)然后再看这篇博客,具体大部分可以见目录哈,然后内部会有多个小Demo帮助你快速学习并上手,后续还将继续更新更多C++相关的内容,敬请期待!

早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。**

文章重点摘抄🗒️

  1. QThread 要想创建线程,就需要创建出这样类的实例然后重写其中的run函数,在run函数中就是多线程执行的内容
  2. 在新线程中是不允许操作ui的,这也是Qt为了防止数据不一致的硬性要求
  3. 多线程的作用处理密集的IO,用其他线程处理这些IO操作而非主线程进行因为IO就代表这可能存在的卡顿情况
  4. 然后就是线程安全相关的一些内容将提及Qt中的锁QMutex和其RAII形式的QMutexLocker、以及条件变量QWaitCondition和信号量QSemaphore

Qt 多线程概述🦜

Linux中学过的多线程API,Linux系统提供的pthread库,Qt中针对系统提供的线程API接口重新封装了

因为Linux原生多线程API设计的非常不方便使用,实际开发中也使用的少

Qt 中的多线程API,就会更方便使用些(参考了Java中的线程库API设计方法)

QThread💘

  • QThread 要想创建线程,就需要创建出这样类的实例
  • 创建线程的时候,需要重点指定线程的入口函数(通过创建一个QThread子类,重写其中的run函数,起到指定入口函数的方式(多态),C++中这种方法不算常见,回调的方式更常见,因为大佬们认为多态机制带来额外开销,也就是查询虚函数表)

常用API:

函数 说明
run() 线程的⼊⼝函数...
start() 通过调⽤ run() 开始执⾏线程。操作系统将根据优先级参数调度线程。如果线程已经在运⾏,这个函数什么也不做。 真正调用系统创建线程的API函数,线程就会执行run
currentThread() 返回⼀个指向管理当前执⾏线程的 QThread的指针。
isRunning() 如果线程正在运⾏则返回true;否则返回false。
sleep() / msleep() / usleep() 使线程休眠,单位为秒 / 毫秒 / 微秒
wait() 阻塞线程,直到满⾜以下任何⼀个条件: 与此 QThread 对象关联的线程已经完成执⾏(即当它从run()返回时)。如果线程已经完成,这个函数将返回 true。如果线程尚未启动,它也返回 true。 已经过了⼏毫秒。如果时间是 ULONG_MAX(默认值),那么等待永远不会超时(线程必须从run()返回)。如果等待超时,此函数将返回 false。 这提供了与 POSIX pthread_join() 函数类似的能。
terminate() 终⽌线程的执⾏。线程可以⽴即终⽌,也可以不⽴即终⽌,这取决于操作系统的调度策略。在terminate() 之后使⽤ QThread::wait() 来确保。
finished() 当线程结束时会发出该信号,可以通过该信号来实现线程的清理⼯作。

实操Demo🗺️

之前基于定时器,写过倒计时这样的程序,也可以通过线程,来完成类似的功能

定时器内部本质也是可以基于多线程来实现的

  • 创建另一个线程,新线程中,进行计时(搞一个循环,每循环异常,sleep 1s,然后更新界面)
  1. 拖拽一个LCD,初始值设置为10
  2. 新增一个C++ Class类,继承QThread父类(Widget、Q_OBJECT)
  3. 重写父类的 run 方法 void run()
  4. 添加函数的定义,新线程是不能直接修改界面内容(Qt只允许主线程操作控件,不允许其他线程对控件进行修改)
  5. 虽然不能修改界面,但是可以针对时间进行计时,每当到了1s,通过信号槽,通知主线程更新的界面内容
  6. 循环10次,并休眠1s,可以直接调用sleep,它本身是Qthread的成员函数
  7. 发送一个信号,告诉主线程
  8. 在类中创建notify信号成员函数(只用写一个函数声明即可Qt会自动定义)
  9. 发送信号notify

回到Widget中

  1. 添加刚刚创建的 Thread类的成员变量
  2. 创建notify信号的信号槽,通过槽函数修改lcd的值
  3. 构造函数中通过该成员变量调用 start启动线程
  4. 槽函数(修改界面的内容)
    1. 先获取lcd中的值
    2. value- -,并再次设置回lcd中

主要源码如下:

cpp 复制代码
//thread.h
#ifndef THREAD_H
#define THREAD_H

#include <QWidget>
#include <QThread>
class Thread : public QThread
{
    Q_OBJECT
public:
    Thread();
    void run();
 signals:
    void notify();
};

#endif // THREAD_H


//widget.h
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include "thread.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    Ui::Widget *ui;
    Thread thread;
private slots:
    void update_lcd();
};
#endif // WIDGET_H



//thread.cpp
#include "thread.h"

Thread::Thread()
{

}

void Thread::run()
{
    for(int i = 0; i < 10;i++)
    {
        QThread::sleep(1);
        emit notify();
    }
}


//widget.cpp
#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    connect(&thread,&Thread::notify,this,&Widget::update_lcd);
    thread.start();
}

Widget::~Widget()
{
    delete ui;
}

void Widget::update_lcd()
{
    int val = ui->lcdNumber->intValue();
    val--;
    ui->lcdNumber->display(val);
}

效果如下:

为什么使用多线程🤔

  • 之前学习多线程,主要站在服务器开发的角度来看待,此时多线程的主要目的是充分利用CPU的计算资源。双路CPU(一个主板两个CPU)
  • 但站在客户端,多线程仍然非常有意义,但侧重点不一样,对于普通用户来说 "使用体验" 时非常重要的
  • 客户端并不是让多线程把CPU计算资源吃完导致卡顿
  • 而是通过多线程的方式,执行一些耗时的等待IO操作,避免主线程被卡死,影响体验
  • 比如客户端要上传/下载一个很大的文件,传输需要很久,此时的这种操作就是 密集的IO操作,他会时程序被系统阻塞,挂起,一旦进程被挂起就意味着用户进行的各种操作程序无法响应
  • 如:启动大型游戏,启动过程中就需要从文件/网络加载大量资源,如果鼠标狂点窗口,就很有可能窗口就僵住了,Windows提示你这个窗口不能响应,要不要强制结束...
  • 因此,更好的做法是使用单独的线程来处理这种密集的IO操作,挂起的这个线程并不会响应主线程的工作,主线程主要负责一个事件循环,负责处理用户的各种操作,就不会出现上述问题。

总结来说对于Qt中遇到的一些密集的IO时,就可以使用多线程的方式来执行!

线程安全问题⚠️

多线程程序会比正常的单执行流的程序来说太复杂了,所以通常会需要保证数据一致问题,所以这个时候为了避免CPU切换过程中导致在该线程数据被其他线程修改,所以此时就需要:

加锁QMutex🔒

把多个线程要访问的公共资源,通过锁保护起来,把并发执行又变成串行执行

  • Qt 同样提供了对应的锁,来针对系统提供的锁进行封装
  • 主要方法也就是:lock加锁、unlock解锁

实操Demo🗺️

先不加锁情况:

  1. 创建一个Tread类
  2. 添加一个static成员,作为公共修改的变量num
  3. 重写run:
    1. 循环100000次
    2. 针对num进行增加

回到Widget

  1. 构造函数中,创建两个线程对象,并启动
  2. 注意三个线程是并发执行的,t1、t2,主线程
  3. 所以需要加上线程的等待,让主线程等待着两个线程执行结束后:
  4. 打印结果
    1. 应该是200000才对,但发现并不对(如下图,源码也如下)
    2. 这就是因为线程切换导致数据不一致!!
cpp 复制代码
//Thread.h
#ifndef THREAD_H
#define THREAD_H

#include <QWidget>
#include <QThread>
class Thread : public QThread
{
    Q_OBJECT
public:
    Thread();
//    void run();//thread练习
    void run();
    static int num;
 signals:
    void notify();
};


#endif // THREAD_H



//widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QThread>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    Thread thread1;
    Thread thread2;

//    connect(&thread,&Thread::notify,this,&Widget::update_lcd);
    thread1.start();
    thread2.start();
    //注意主线程和其他新线程是同步的所以这里要等待下才能看到结果!!!!
    QThread::sleep(2);
    qDebug() << thread1.num;
}

Widget::~Widget()
{
    delete ui;
}

加上锁:

  1. 在Thread中创建静态的锁对象(注意一定要静态保证使用的是同一把锁,保证多个线程使用用一个锁对象!!)
  2. 再在 num++ 之前进行加锁解锁操作
  3. 此时在运行就能发现变成了 200000

其中回顾可能忘记释放锁的情况:

QMutexLocker(锁RAII)

将mutex.lock()代码替换为QMutexLocker对象,构造传递的就是mutex锁对象

  • 当然Qt中也能使用C++中的锁进行上述操作
  • 但并不太建议这种混着用的操作

至于后面的条件变量和信号量这里就不过多介绍了,也很简单,本质就是查文档结合基本知识就能使用


条件变量QWaitCondition、信号量QSemaphore

这里和Linux中谈到的是完全一致的,多个线程之间的调度是无序的,为了能够一定程度的干预线程之间的执行顺序,保证一定的同步,从而引入条件变量

条件变量

  • 等待条件满足:wait(注意需要循环判断)
  • 释放锁 + 等待
  • 等待条件满足并释放锁
  • 唤醒:wake、wakeAll
    核心方法:
cpp 复制代码
class QWaitCondition {
public:
    // 等待,释放mutex,进入等待状态
    bool wait(QMutex *mutex, unsigned long time = ULONG_MAX);
    
    // 唤醒一个等待的线程
    void wakeOne();
    
    // 唤醒所有等待的线程
    void wakeAll();
};

信号量:

  • P操作:acquire
  • V操作:release

核心方法:

cpp 复制代码
class QSemaphore {
public:
    QSemaphore(int n = 0);  // 初始化信号量值
    
    // 获取n个资源(减少信号量)
    void acquire(int n = 1);
    
    // 尝试获取资源,不阻塞
    bool tryAcquire(int n = 1);
    
    // 尝试在超时时间内获取
    bool tryAcquire(int n, int timeout);
    
    // 释放n个资源(增加信号量)
    void release(int n = 1);
    
    // 获取当前可用资源数
    int available() const;
};

本章完。预知后事如何,暂听下回分解。

如果有任何问题欢迎讨论哈!

如果觉得这篇文章对你有所帮助的话点点赞吧!

持续更新大量C++ Qt细致内容,早关注不迷路。

相关推荐
SimonKing几秒前
基于Netty的TCP协议的Socket服务端
java·后端·程序员
2301_773730314 分钟前
系统编程—在线商城信息查询系统
c++·html
郝学胜-神的一滴5 分钟前
深入理解Linux中的Try锁机制
linux·服务器·开发语言·c++·程序人生
柒.梧.7 分钟前
Spring Boot集成JWT Token实现认证授权完整实践
java·spring boot·后端
环黄金线HHJX.28 分钟前
拼音字母量子编程PQLAiQt架构”这一概念。结合上下文《QuantumTuan ⇆ QT:Qt》
开发语言·人工智能·qt·编辑器·量子计算
qq_2562470533 分钟前
除了“温度”,如何用 Penalty (惩罚) 治好 AI 的“复读机”毛病?
后端
内存不泄露43 分钟前
基于Spring Boot和Vue 3的智能心理健康咨询平台设计与实现
vue.js·spring boot·后端
qq_124987075344 分钟前
基于Spring Boot的电影票网上购票系统的设计与实现(源码+论文+部署+安装)
java·大数据·spring boot·后端·spring·毕业设计·计算机毕业设计
散峰而望1 小时前
【算法竞赛】顺序表和vector
c语言·开发语言·数据结构·c++·人工智能·算法·github
麦兜*1 小时前
【Spring Boot】 接口性能优化“十板斧”:从数据库连接到 JVM 调优的全链路提升
java·大数据·数据库·spring boot·后端·spring cloud·性能优化