🌈 个人主页:Zfox_
🔥 系列专栏:Qt

一:🔥 Qt 多线程概述

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

  • QThread 代表一个在应用程序中可以 独立控制 的线程,也可以和进程中的其他线程 共享数据
  • QThread 对象管理程序中的一个控制线程。

二:🔥 QThread 常用 API

方法名 作用
run() 线程入口函数
start() 通过调用 run() 开始执行线程,操作系统会根据 优先级参数调度 线程。如果线程正在运行,则这个函数什么都不会做
currentThread() 返回一个指向管理当前执行线程的 QThread 的指针
isRunning() 如果线程正在运行返回 true,否则反之
sleep() / msleep() / usleep() 使线程休眠,单位为 秒/毫秒/微秒
wait() 阻塞线程,直到满足以下任何一个条件:
与此 QThread 对象关联的线程已经完成执行(即当它从run()返回时)。如果线程已经完成,这个函数将返回 true。如果线程尚未启动,它也返回 true。
已经过了几毫秒。如果时间是 ULONG MAX(默认值),那么等待永远不会超时(线程必须从run()返回)。如果等待超时,此函数将返回false。
这提供了与 POSIX pthread_join()函数类似的功能。
terminate() 终止线程执行。(可以选择立即终止,也可以不立即终止)
取决于操作系统的 调度策略,在 terminate() 之后使用 QThread::wait() 来确保
finished() 当线程结束时会发出该信号,通过其来实现线程清理工作

三:🔥 线程使用

【实现倒计时页面】

1、创建项目,以 Widget 作为基类,设计 UI 界面如下:

image-20250131195525774

2、新建一个类,并且继承于 QThread 类

image-20250131195801401

3、thread.h 声明实现如下:

image-20250131200711715

run 方法 在 thread.cpp 实现如下:

void Thread::run()
{
    // 这里我们不能直接去修改界面内容
    // 原因:存在线程安全问题,Qt 针对界面的控件状态的任何修改必须在 主函数 中进行

    // 这里我们就仅仅针对时间进行计时即可
    // 每隔一秒 通知主线程更新界面内容
    for(int i = 0; i < 10; i++){
        sleep(1);
        // 发送一个信号 通知主线程
        emit notify();
    }
}

4、Widget 声明实现如下:

image-20250131201008609

Widget.cpp 代码如下:

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

    // 连接信号槽,通过槽函数更新界面
    connect(&thread, &Thread::notify, this, &Widget::handle);

    // 启动线程
    thread.start();
}

void Widget::handle()
{
    // 此处修改界面内容
    int val = ui->lcdNumber->intValue();
    val--;
    ui->lcdNumber->display(val);
}
  • 演示这里就省略了,大家可以自行演示结果

💡 说明:

  1. 线程函数内部不允许操作 UI 图形界面,一般用数据处理;
  2. connect()函数第五个参数表示的为连接的方式,且只有在多线程的时候才意义。

connect() 函数第五个参数为 Qt::ConnectionType,用于指定信号和槽的连接类型。同时影响信号的传递方式和槽函数的执行顺序。

Qt::ConnectionType 提供了以下五种方式:

名称 作用
Qt::AutoConnection 在 Qt中,会根据信号和槽函数所在的线程 自动选择 连接类型。如果信号和槽函数在同一线程中,那么使用 Qt:DirectConnection 类型;如果它们位于不同的线程中,那么使用Ot::QueuedConnection 类型
Qt::DirectConnection 当信号发出时,槽函数会 立即在同一线程中执行 。这种连接类型适用于信号和槽函数在同一线程中的情况,可以实现直接的函数调用,但需要注意线程安全性
Qt::QueuedConnection 当信号发出时,槽函数会被插入到接收对象所属的线程的事件队列中,等待下一次事件循环时执行。这种连接类型适用于信号和槽函数在不同线程中的情况,可以确保线程安全
Qt::BlockingQueuedConnection Qt:QueuedConnection 类似,但是发送信号的线程会被 阻塞,直到函数执行完毕,这种连接类型适用于需要等待槽函数执行完毕再继续的场景,但需要注意可能引起 线程死锁 的风险。
Qt::UniqueConnection 这是一个标志,可以使用 位或 与上述任何一种连接类型组合使用

四:🔥 线程安全

实现线程互斥和同步常用的类有:

  • 互斥锁:QMutex、QMutexLocker.
  • 条件变量:QWaitCondition
  • 信号量:QSemaphore
  • 读写锁:QReadLocker、QWriteLocker、OReadWriteLock

🦋 互斥锁

互斥锁是一种保护和防止多个线程同时访问同一对象实例的方法,在Qt中,互斥锁主要是通过 QMutex 类来处理。

QMutex

  • 特点:QMutex 是Qt框架提供的互斥锁类,用于 保护共享资源 的访问,实现线程间的互斥操作。
  • 用途:在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全。
QMutex mutex;
mutex.lock(); //上锁
//访问共享资源
//...
mutex.unlock(); //解锁

【案例:多线程自加】

先创建一个项目, 以 QWidget 作为基类,然后再创建C++ Class文件 Thread(新建一个类,并且继承于 QThread 类),和线程使用那步骤类似,如下:

声明如下

image-20250201152005421

run 方法 在 Thread.cpp 实现如下:

// 头文件既要声明,.cpp 文件也要定义
int Thread::num = 0;
QMutex Thread::mutex;

void Thread::run()
{
    for(int i = 0; i < 50000; i++){
        mutex.lock();
        num++;
        mutex.unlock();
    }
}

Widget.cpp 构造函数如下:

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

    // 创建两个线程对象
    Thread t1, t2;
    t1.start();
    t2.start();

    // 加上两个线程,让主线程等待两线程结束
    t1.wait();
    t2.wait();

    // 打印结果
    qDebug() << Thread::num;
}

最后结果不出意外:100000,如果我们不加锁的话,就会出现意外,可以自行测试

QMutexLocker

  • 特点:QMutexLockerQMutex 的辅助类,使用 RAII(Resource Acquisition ls Initialization) 方式对互斥锁进行 上锁和解锁 操作。
  • 用途:简化对互斥锁的上锁和解锁操作,避免忘记解锁导致的死锁 等问题。

C++ 中引入 智能指针 就是解决这个问题的,C++ 11 引入了 std::lock_guard 相当于是 std::mutex 智能指针,借助了 RAII 机制

QMutex mutex;
{
	QMutexLocker locker(&mutex)://在作用域内自动上锁
	// 访间共享资源
    // ...
}//在作用城结束时自动解锁

如下,借助上面互斥锁的例子,做点修改

image-20250201152456305

QReadWriteLocker、QReadLocker、QWriteLocker

特点:

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

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

QReadWriteLock rwLock;
//在读操作中使用读锁
{
    QReadLocker locker(&rwLock); //在作用域内自动上读锁
    //读取共享资源
    //...
} //在作用域结束时自动解读锁

//在写操作中使用写锁
{
    QWriteLocker locker(&rwLock); //在作用用域内自动上写锁
    //修改共享资源
    //...
} //在作用域结束时自动解写锁

🦋 条件变量

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

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

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

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

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

QMutex mutex;
QWaitCondition condition;

//在等待线程中
mutex.lock();
//检查条件是否满足,若不满足则等待
while (!conditionFullfilled())
{
	condition.wait(&mutex); //等待条件满足并释放锁
}

//条件满足后继续执⾏
//...
mutex.unlock();

//在改变条件的线程中
mutex.lock();

//改变条件
changeCondition();
condition.wakeAll(); //唤醒等待的线程
mutex.unlock();
  • 注意:这里使用 while 判定 而不是 if
  • 因为唤醒之后 需要再确认一下当前条件是否真的成立,wait 可能被提前唤醒了

🦋 信号量

有时在多线程编程中,需要确保多个线程可以相应的 访问一个数量有限的相同资源

例如,运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这一事实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用 信号量 来处理。

信号量类似于 增强 的互斥锁(Plus 版),不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量

  • 特点:QSemaphore 是 Ot框架提供的计数信号量类,用于控制同时访问共享资源的线程数量。
  • 用途:限制并发线程数量,用于解决一些资源有限的问题。
QSemaphore semaphore(2); //同时允许两个线程访问共享资源

//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞

//访问共享资源
//...

semaphore.release(); //释放信号量
//在另⼀个线程中进⾏类似操作

五:🔥 共勉

😋 以上就是我对 【Qt】 多线程 的理解, 觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~ 😉
在这里插入图片描述

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐