【Qt】Qt多线程编程指南:提升应用性能与用户体验

文章目录

  • 前言
  • 1. Qt 多线程概述
  • 2. QThread 常用 API
  • 3. 使用线程
  • 4. 多线的使用场景
  • 5. 线程安全问题
    • 5.1. 加锁
    • 5.2. QReadWriteLocker、QReadLocker、QWriteLocker
  • 6. 条件变量 与 信号量
    • 6.1. 条件变量
    • 6.2 信号量
  • 总结

前言

在现代软件开发中,多线程编程已成为一个不可或缺的技能,尤其是在需要处理复杂任务和提高应用程序性能的场合。Qt,作为一个跨平台的应用程序框架,提供了强大的多线程支持,使得开发者能够充分利用多核处理器的优势,开发出响应迅速且高效的应用程序。本文将深入探讨Qt多线程的基本概念、API使用、线程安全问题以及同步机制,旨在帮助开发者更好地理解和运用Qt的多线程功能。

1. Qt 多线程概述

Qt 多线程 和 Linux 中线程,本质是一个东西。
Linux 中的各种和线程相关的 原理 和 注意事项,都是在Qt中适用的。

Qt 中的多线程 API
Linux 中的多线程 API,Linux 系统提供的 pthread 库,Qt 中针对系统提供的线程 API 重新封装了。
C++ 11 中,也引入了线程 std::thread

Linux 原生多线程 API,设计的非常差,使用起来非常不方便(也是 C 语言本身的局限性引起的)实际开发中,很少使用原生 api

std::thread 要比 Linux 的 API 要更好一些
Qt 中的多线程 API,还要好一点,其实参考了 Java 中的线程库 API 的设计方式。

QThread 要想创建线程,就要创建出这样的实例,创建线程的时候,需要重点指定线程的入口函数。创建一个 QThread 的子类,重写其中 run 函数,起到指定函数入口的方式(多态)
(C++ 中这种做法,不算特别常见,相比之下 std::thread 直接指定回调的方式更常见一些,有些 C++ 的大佬,认为多态机制,带来运行时的额外开销(运行时,查询虚函数表,找到对应的函数再执行))
有些场景确实对于性能追求到极致(游戏引擎,AI,做高性能服务器…)
Qt 做客户端开发,客户端性能只要不太拉跨就行!

性能从来不是Qt优先追求的

2. QThread 常用 API

在这里插入图片描述
start(): 这个操作就是真正调用系统 API 创建线程,新的线程创建出来之后自然就会自动执行 run 函数。
可以使用 wait, 让一个线程等待另一个线程执行结束

3. 使用线程

实例:
之前基于定时器,写过倒计时这样的程序。
也可以通过线程,来完成类似的功能。定时器内部本质上也是可以基于多线程来实现的。(Qt 的定时器是否基于多线程,不太清楚)

创建另一个线程,新线程中,进行计时(搞一个循环,每循环一次,sleep 1s,sleep完成,就可以更新界面了)
在这里插入图片描述
在这里插入图片描述
由于存在线程安全问题,多个线程时对于界面的状态进行修改,此时就会导致界面就出错了。Qt选择了一刀切!针对界面控件状态进行任何修改,务必在主线程中执行。

// 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();

    Thread thread;

    void handle();
private:
    Ui::Widget *ui;
};
#endif // WIDGET_H
// 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::handle);

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

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

void Widget::handle()
{
    // 此处修改界面内容
    int value = ui->lcdNumber->intValue();
    value--;
    ui->lcdNumber->display(value);
}
// thread.h
#ifndef THREAD_H
#define THREAD_H

#include <QWidget>
#include <QThread>

class Thread : public QThread
{
    Q_OBJECT
public:
    Thread();

    // 要用的目的是重写父类的方法 run 方法
    void run();

signals:
    void notify(); // 只用声明不用定义
};

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

Thread::Thread()
{

}

void Thread::run()
{
    // 在这个 run 中。能否直接去进行修改界面内容呢?
    // 不可以!!!
    // 虽然不可以修改界面,但是可以针对时间来进行计时
    // 当每到一秒钟的时候,通过信号槽,来通知主线程,负责更新界面内容

    for (int i = 0; i < 10; ++i) {
        // sleep 本身是 QThead 的成员函数, 就可以直接调用
        sleep(1);
        // 发送一个信号,通知主线程
        emit notify();
    }
}

在这里插入图片描述
在这里插入图片描述

4. 多线的使用场景

之前学习多线程,主要还是站在服务器开发的角度来看待的。
当时谈到多线程,最主要的目的,是为了充分利用多核 CPU 的计算资源。双路 CPU(一个主板上有两个CPU)。
客户端,多线程仍然非常有意义,侧重点就不同了,对于普通用户来说,“使用体验”是一个非常重要的话题。
如果“非常快”的代价是“系统很卡”用户大概率是不会买账的,虽然普通用户的家用 PC 上也是多核CPU,客户端上的程序很少会使用多线程把 CPU 计算资源吃完。
相比之下,客户端的多线程,主要是用于,通过多线程的方式,执行一些耗时的操作,避免主线程被卡死,避免对用户造成一些不好的体验。
比方说,客户端经常会和服务器进行网络通信,比方说客户端要上传/下载一个很大的文件,传输需要好久(20分钟)(像这样就是算是密集的IO操作,比如代码中持续不断的进行 QFile.write)这种密集 IO 就会使程序被系统阻塞,挂起;一旦进程都被挂起了,此时意味着,用户进行各种操作,程序都无响应。(比如,启动吃鸡,启动过程中就需要从文件/网络 加载大量的资源,此时如果你狂点鼠标窗口,很可能这个窗口就僵住了)“WIndows 提示你这个窗口不能响应,是否要强制结束!”
因此,相比之下,更好的做法,使用单独的线程,来处理这种密集 IO 操作,要挂起也是挂起这个新的线程。主线程负责用户的各种操作,此时主线程仍然可以继续工作,继续响应用户的各种操作。

5. 线程安全问题

多线程程序太复杂了

5.1. 加锁

把多个线程访问的公共资源,通过锁保护起来。把并发执行变成串行执行。
Linux: mutex 互斥量。
C++11: 引入 std::mutex
Qt 同样也提供了对应的锁,来针对系统提供的锁进行封装。
QMutex: lock 加锁, unlock 解锁。

void Thread::run()
{
    for (int i = 0; i < 50000; ++i) {
        ++num;
    }
}
#include "mainwindow.h"
#include "ui_mainwindow.h"

#include "thread.h"
#include <QDebug>

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

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

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

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

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

由于三个线程之间是并发执行的关系,当 t1 和 t2 运行起来之后,主线程仍然会继续往后执行,执行到打印的时候,大概率 t1、t2 还没执行呢,所以要加上wait,进行阻塞等待!
在这里插入图片描述
最后打印出来的结果并不是我们预期中的100,000 !说明是存在bug,说明是存在线程安全问题!

// 创建锁对象
static QMutex mutex; 

多个线程加锁的对象,得是同一个对象!不同对象,此时不会产生锁的互斥,也就无法把 并发执行 -> 串行执行,也就无法解决上述问题。

#include "thread.h"

int Thread::num = 0;
QMutex Thread::mutex;

Thread::Thread()
{

}

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

++num; 是一个两个线程访问的公共变量,之前如果并发执行,就可能第一个线程修改了一半,第二个线程也进行了修改,就容易出现问题。(++操作对应 三个cpu指令,在操作系统中详细介绍)
加了锁之后,第一个线程顺利拿到锁,继续执行++,在第一个线程没有执行完的时候,第二个线程也尝试枷锁,就会阻塞等待。一直等待到第一个线程加锁,第二个线程才可能从阻塞中被唤醒。
在这里插入图片描述

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

像这里的锁,很容易忘记unlock,实际开发中, 加锁之后,涉及到的逻辑可能很复杂,下面很容易就忘记释放锁。
比如下面,也算是没释放锁:

mutex.lock();
if (...) {
	return;
}
mutex.unlock();

或者,抛出异常,释放动态内存,也会存在类似的问题。
C++ 引入 智能指针,就是为了解决上述的问题。
C++11 引入了 std::lock_guard, 相当于是 std::lock_guard, 相当于是 std::mutex 智能指针, 借助 RAII 机制。

{
	std::lock_guard guard(mutex);
	// 执行各种逻辑...
} // 大括号执行完毕,guard 变量的声明周期结束,在析构的时候,执行unlock了。

上述方案,Qt 也参考过来了: QMutexLocker

#include "thread.h"
#include <QMutexLocker>

int Thread::num = 0;
QMutex Thread::mutex;

Thread::Thread()
{

}

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

Qt 的锁 和 C++标准库中的锁,本质上都是封装的系统提供的锁,编写多线程代码的时候,可以使用 Qt 的锁,也可以使用 C++ 的锁。
C++ 的锁能锁Qt 的线程吗? 是可以的!(虽然混着用也行,但一般不建议)

5.2. QReadWriteLocker、QReadLocker、QWriteLocker

特点
QReadWriteLock 是读写锁类,用于控制读和写的并发访问。
QReadLocker 用于读操作上锁,允许多个线程同时读取共享资源。
QWriteLocker 用于写操作上锁,只允许一个线程写入共享资源。
用途:在某些情况下,多个线程可以同时读取共享数据,但只有一个线程能够进行写操作。读写锁提供了更高效的并发访问方式。

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

6. 条件变量 与 信号量

Qt 中的条件变量 与 信号量 和 Linux 中的条件变量、信号量一致。

6.1. 条件变量

多个线程,之间调度是无序的,为了能够一定程度干预线程之间的顺序引入条件变量。
在 Qt 中,专门提供了 QWaitCondition类 来解决像上述这样的问题。
wait:中就会先释放锁 + 等待
要想释放锁,前提就是先获取到锁。

QMutex mutex;
QWaitCondition condition;
//在等待线程中
mutex.lock();
//检查条件是否满⾜,若不满⾜则等待
while (!conditionFullfilled()) // 
{
 condition.wait(&mutex); //等待条件满⾜并释放锁
}
//条件满⾜后继续执⾏
//...
mutex.unlock();
//在改变条件的线程中
mutex.lock();
//改变条件
changeCondition();
condition.wakeAll(); //唤醒等待的线程
mutex.unlock();

判定线程继续执行的条件是否成立,不成立就进行wait等待。
这里要使用 while 判定而不是 if,因为唤醒之后还需要确认一下当前条件是不是真的成立了。wait 可能被提前唤醒(可能被信号打断了)

6.2 信号量

有时在多线程编程中,需要确保多个线程可以相应的访问⼀个数量有限的相同资源。例如,运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这⼀事实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用信号量来处理。信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量。
特点:QSemaphore 是 Qt 框架提供的计数信号量类,用于控制同时访问共享资源的线程数量。
用途:限制并发线程数量,用于解决⼀些资源有限的问题。

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

总结

本文详细介绍了Qt多线程的各个方面,从基础概念到实际应用,再到线程安全和同步机制的讨论。首先,我们概述了Qt多线程与Linux线程的关系,并比较了Qt、C++11和Linux原生API的优缺点。接着,我们深入探讨了QThread的常用API和如何使用线程来执行耗时操作,同时强调了Qt中界面更新必须在主线程中进行的原则。

在多线程的使用场景中,我们讨论了多线程在客户端开发中的重要性,尤其是在提升用户体验方面的作用。随后,文章重点讨论了线程安全问题,包括加锁机制、读写锁以及条件变量和信号量的使用,这些都是确保多线程程序正确运行的关键技术。

最后,通过实际代码示例,我们展示了如何在Qt中创建和管理线程,以及如何使用锁和其他同步机制来处理线程间的通信和数据共享。通过本文的学习,开发者应该能够更加自信地在Qt中实现多线程编程,编写出既高效又稳定的应用程序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/747405.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数字图像分析(第三部分)

文章目录 第11章 基于概率图模型的图像分析概率有向图模型因子分解生成式模型链式图条件独立性有向图模型的马尔科夫毯概率无向图模型模型定义概率无向图模型的因子分解条件随机场条件随机场的定义条件随机场的预测算法第12章 运动分析运动相机建模光流运动表达方法运动估计准则…

AI软件革新文本操作体验:从自动粘贴文本到一键提取保存手机号码

在当今数字化时代&#xff0c;AI技术的快速发展为各行各业带来了革命性的变革。特别是在文本处理领域&#xff0c;AI软件通过其强大的自动粘贴文本功能以及一键提取并保存手机号码的便捷操作&#xff0c;极大地提高了工作效率&#xff0c;为用户带来了全新的体验。本文将深入探…

CSS 文本输入框右下角的尺寸控件(三斜线:-webkit-resizer)消除,以及如何配置其样式,添加 resize 让标签元素可进行拖拽放大。

前言&#xff1a;在日常的前端开发中&#xff0c;不管是原始的和 还在在各类组件库中的文本输入框中&#xff0c;元素内容的右下角总是有一个三斜线的样式&#xff0c;本文简单了解它是什么&#xff1f;如何去控制并修改样式&#xff1f; 一、它是&#xff1f; 这三个斜线其实…

v0.9.6 开源跨平台个人知识管理工具 TidGi-Desktop

在这个信息爆炸的时代&#xff0c;知识管理变得尤为重要。太记(TidGi)&#xff0c;一款基于太微(TiddlyWiki)的知识管理桌面应用&#xff0c;正是为了满足人们对信息整理、知识管理和个人隐私保护的需求而设计的。它不仅能够帮助用户高效地管理和整理信息&#xff0c;还能够自动…

简化部署流程——无线UWB如何实现自标定?

一.什么是UWB信标自标定&#xff1f; UWB&#xff08;超宽带&#xff09;自标定是指在UWB系统中&#xff0c;基站或节点能够自动识别和确定自己的位置&#xff0c;无需外部干预或手动输入其地理位置信息。这种技术主要利用系统内部的信号测量和算法来自动计算节点之间的距离以…

使用PEFT库进行ChatGLM3-6B模型的LORA高效微调

PEFT库进行ChatGLM3-6B模型LORA高效微调 LORA微调ChatGLM3-6B模型安装相关库使用ChatGLM3-6B模型GPU显存占用准备数据集加载模型加载数据集数据处理数据集处理配置LoRA配置训练超参数开始训练保存LoRA模型模型推理从新加载合并模型使用微调后的模型 LORA微调ChatGLM3-6B模型 本…

【vue】vue响应式原理

vue响应式原理 vue2的响应式原理 vue2对对象类型的监听是通过Object.defineProperty实现的&#xff0c;给想要实现响应式的数据对象每个属性加上get,set方法&#xff0c;以实现数据劫持的操作。而对数组类型的监听是通过重写数组的方法实现的。 Object.defineProperty的定义…

组合数学、圆排列、离散数学多重集合笔记

自用 如果能帮到您&#xff0c;那也值得高兴 知识点 离散数学经典题目 多重集合组合 补充容斥原理公式 隔板法题目 全排列题目&#xff1a;

机械拆装-基于Unity-准备零件

目录 前言 1. 装配体模型的准备&#xff08;STEP格式保存为零件&#xff09; 1.1 关于不停提示“默认模板无效” 1.2 关于无法保存单个零件的解决 2. 整理装配体与零件 2.1 零件命名规则 2.2 建立子装配体 3. 装配体和零件转换格式 3.1 3DMax单位设置 3.2 装配体转换 3.3…

JavaScript通用下载方法,但jpg图片下载打不开

通用下载方法&#xff0c;通过Blob的方式&#xff0c;访问Url地址&#xff0c;下载对应的图片&#xff0c;excel等文件。 axios({method: "get",url,responseType: "blob",}).then((res: any) > {const link document.createElement("a");co…

Linux - 札记 - W10: Warning: Changing a readonly file

Linux - 札记 - W10: Warning: Changing a readonly file 这里写目录标题 一、问题描述1. 现象2. 原因 二、解决方案 一、问题描述 1. 现象 在使用 vim 编辑文件时&#xff08;我这里是要编辑 /root/.ssh/authorized_keys&#xff09;提示&#xff1a;W10: Warning: Changing…

VOC格式转YOLO格式,xml文件转txt文件简单通用代码

目录 前言 思路介绍 代码 完整代码 拓展代码 前言 很多人在进行目标检测训练时习惯将得到的数据标注为XML文件的VOC格式&#xff0c;或者在网上获取的数据集被标注为XML文件&#xff0c;但是不同的标注工具进行的标注会产生不同的标注xml文件&#xff0c;这里我写了一种通用…

信息学奥赛初赛天天练-36-CSP-J2021阅读程序-ASCII、运算符优先级、二进制补码存储、模拟算法应用

PDF文档公众号回复关键字:20240626 2021 CSP-J 阅读程序2 1 阅读程序(判断题1.5分 选择题3分 共计40分 ) #include<stdio.h> #include<string.h>char base[64]; char table[256]; char str[256]; char ans[256];void init() {for(int i0;i<26;i) base[i]Ai;fo…

49、基于归一化感知器的输入向量分类(matlab)

1、基于归一化感知器的输入向量分类的原理及流程 归一化感知器是一种分类算法&#xff0c;其原理基于感知器算法&#xff0c;但是在输入向量上进行了归一化处理&#xff0c;以提高算法的性能和稳定性。 流程如下&#xff1a; 输入向量归一化&#xff1a;对每个输入向量进行归…

图解HTTP笔记整理(前六章)

图解HTTP 第一章 web使用HTTP &#xff08;HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff09;协议作文规范&#xff0c;完成从客户端到服务器端等一系列运作流程。 协议&#xff1a;计算机与网络设备要相互通信&#xff0c;双方就必须基于相同的方法。比如…

JetBrains Rider 2024安装教程

一、下载Rider 1、进入官网&#xff0c;点击“下载” 2、下载完毕 二、安装Rider 1、双击下载的exe文件 2、点击“下一步” 3、可以点击“浏览”选择安装路径&#xff0c;之后点击“下一步” 4、选中图中四项&#xff0c;点击“下一步” 5、选中图中四项&#xff0c;点击“下…

Superset二次开发之导入导出功能源码解读

可导出的类型 支持 看板(Dashboard)、图表(Charts)、数据集(Datasets)、SQL(saved_query)、数据库(Database connection) 单次或批量的导出,和单次导入操作 看板(Dashboard) 图表(Charts) 数据集(Datasets) SQL (saved_query) 数据库(database connections)…

4.任务调度

1.基本知识 2.任务的状态 FreeRTOS中任务共存在4种状态&#xff1a;Running 运行态 当任务处于实际运行状态称之为运行态&#xff0c;即CPU的使用权被这个任务占用&#xff08;同一时间仅一个任务处于运行态&#xff09;。Ready 就绪态 处于就绪态的任务是指那些能够运行&…

声场合成新方法:基于声波传播的框架

声场合成是指在房间内的麦克风阵列上&#xff0c;根据来自房间内其他位置的声源信号&#xff0c;合成每个麦克风的音频信号。它是评估语音/音频通信设备性能指标的关键任务&#xff0c;因为它是一种成本效益高的方法&#xff0c;用于数据生成以替代真实的数据收集&#xff0c;后…

Java知识点整理 13 — Hutool工具库

在开发时经常需要编写很多与业务无关的代码&#xff0c;比如获取指定日期对象、获取本机 IP 地址、数据加密等。通常我们会将这些代码独立出来&#xff0c;放到 utils 目录下&#xff0c;作为工具类供其它代码调用。 但如果遇到一个从未接触过的领域知识&#xff0c;开发一个新…