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

news2024/10/7 18:24:04

文章目录

  • 前言
  • 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.coloradmin.cn/o/1866023.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

越有水平的领导,越擅长用这3个字来管人,怪不得执行力强

越有水平的领导&#xff0c;越擅长用这3个字来管人&#xff0c;怪不得执行力强 第一个字&#xff1a;“实” 要想提高执行力&#xff0c;必须发扬务实、实干、刻苦勤勉的工作精神。纸上谈兵&#xff0c;夸夸其谈的事情少做&#xff0c;多行动&#xff0c;少说话。 沉浸在表面…

问界M9累计大定破10万,创中国豪车新纪录

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 更多资源欢迎关注 6月26日消息&#xff0c;华为常务董事、终端BG董事长、智能汽车解决方案BU董事长余承东今日宣布&#xff0c;问界M9上市6个月&#xff0c;累计大定突破10万辆。 这一成绩&#xff0c;也创造了中国市场…

视觉分割的定义与性能度量

文章目录 视觉分割的定义语义分割(Semantic Segmentation)实例分割(instance Segmentation)全景分割(Panoptic Segmentation)视频语义分割(Video Semantic Segmentation)视频实例分割(Video instance Segmentation)视频全景分割(Video Panoptic Segmentation)各任务对比 视觉分…

苹果Mac安装adobe软件报错“installer file may be damaged”解决方案

最近Mac电脑系统的有小伙伴在安装PS、AI、AE、PR等软件&#xff0c;出现了一个错误&#xff0c;让人头疼不已&#xff0c;苦苦找寻&#xff0c;也找不到完美的解决方法。让我们来一起看看吧&#xff01; 很多小伙伴都喜欢苹果电脑&#xff0c;但是在安装外来软件时&#xff0c;…

广州数据中心机房搬迁验收要求

1.验收要求 新机房装修工程全部竣工&#xff0c;各类环境设备安装到位&#xff0c;包括空调、UPS、柴油发电机等设备安装调试完毕&#xff0c;机房接地、防雷、消防系统检验合格&#xff0c;机房综合布线工作完成&#xff0c;机房各项环境指标达标&#xff0c;机房整体通过验收…

嵌入式项目分享| 终极智能手表,全过程+全开源分享

这是一个非常完整的智能手表开源项目,功能齐全,且资料开源,如果你是:自己平时喜欢diy的工程师,想要提升开发技能的学生,马上要做毕设的大四学生,这个手表很值得一做,别错过了~~ 所有开源的资料以及原文链接见文末。 先来看下这个手表的功能: 首先,是一个可以佩戴的手…

进程、CPU、MMU与PCB之间的关系

目录 进程与cpu&#xff08;中央处理器&#xff09; 源代码、程序、cpu与进程的关系 cpu超线程 CPU的简易架构与处理数据过程 进程与MMU&#xff08;内存管理单元&#xff09; mmu作用 cpu和mmu的关系 进程与PCB&#xff08;进程控制块&#xff09; PCB介绍与内部成员…

PMBOK® 第六版 实施整体变更控制

目录 读后感—PMBOK第六版 目录 对于变化的态度&#xff0c;个人引用两句加以阐释&#xff0c;即“流水不腐&#xff0c;户枢不蠹”与“不以规矩&#xff0c;不能成方圆”。这看似相互矛盾&#xff0c;实则仿若两条腿总是一前一后地行进。有一个典型的例子&#xff0c;“自由美…

CentOS7环境脚本一键安装MySQL8

安装包准备 获取下载地址 选择对应的下载版本&#xff0c;如下图&#xff0c;右键RPM Bundle的Download&#xff0c;复制下载链接地址 下载安装包 [hadoopnode3 installfile]$ wget https://downloads.mysql.com/archives/get/p/23/file/mysql-8.0.31-1.el7.x86_64.rpm-bund…

Android View点击事件分发原理,源码解读

View点击事件分发原理&#xff0c;源码解读 前言1. 原理总结2.1 时序图总结2.2 流程图总结 2. 源码解读2.1 Activity到ViewGroup2.2 ViewGroup事件中断逆序搜索自己处理点击事件ViewGroup总结 2.3 ViewOnTouchListeneronTouchEvent 3. 附录&#xff1a;时序图uml代码 前言 两年…

mysql查询2个日期之间的数据,表字段只有年和月,无日期字段查询的解决

1.核心mysql查询 SELECT * FROM 表名 WHERE CONCAT(year, -, LPAD(month, 2, 0)) > 2022-02-08 AND CONCAT(year, -, LPAD(month, 2, 0)) < 2024-06-06;2.表结构 CREATE TABLE ys_datezzq (id int(10) NOT NULL AUTO_INCREMENT,bid int(10) NOT NULL DEFAULT 0 COMMEN…

如何下载植物大战僵尸杂交版,最全攻略来了

《植物大战僵尸杂交版》由热爱原版游戏的B站UP主“潜艇伟伟迷”独立开发&#xff0c;带来了创新的游戏体验。如果你是策略游戏的爱好者&#xff0c;下面这份全面的下载和游玩攻略将是你的理想选择。 游戏亮点&#xff1a; 杂交植物系统&#xff1a;结合不同植物特性&#xff0c…

ctfshow 新春欢乐赛 web

web1 <?phphighlight_file(__FILE__); error_reporting(0);$content $_GET[content]; file_put_contents($content,<?php exit();.$content);?contentphp://filter/string.rot13|<?cuc flfgrz(yf /);?>|/resourceshell.php绕过死亡exit 但是我发现个问题就是…

6.26作业

1.整理思维导图 2.统计家目录下.c文件的个数 ls ~/*.c | wc -l 3.终端输入一个.sh文件&#xff0c;判断文件是否由可执行权限&#xff0c;如果有可执行权限运行脚本&#xff0c;没有可执行权限添加可执行权限后&#xff0c;再运行脚本 #!/bin/bash read -p "请输入一个.…

【C语言】字符/字符串+内存函数

目录 Ⅰ、字符函数和字符串函数 1 .strlen 2.strcpy 3.strcat 4.strcmp 5.strncpy 6.strncat 7.strncmp 8.strstr 9.strtok 10.strerror 11.字符函数 12. 字符转换函数 Ⅱ、内存函数 1 .memcpy 2.memmove 3.memcmp Ⅰ、字符函数和字符串函数 1 .strlen 函数原型&#xff1a;…

UNIAPP编译到微信小程序时,会多一层以组件命名的标签

UNIAPP编译到微信小程序时&#xff0c;会多一层以组件命名的标签 解决方案 可以配置virtualHost来配置 export default {options: {virtualHost: true} }

OPenFast中AeroDyn,ElastoDyn,ElastoDyn_Tower,ServoDyn的作用!

在OpenFAST中&#xff0c;这四个文件分别有不同的作用&#xff0c;它们用于定义风力涡轮机不同部分的特性和行为。以下是每个文件的总结及其作用&#xff1a; NRELOffshrBsline5MW_Onshore_AeroDyn15.dat 作用&#xff1a;这是AeroDyn模块的输入文件&#xff0c;用于定义风力涡…

【Day03】0基础微信小程序入门-学习笔记

文章目录 视图与逻辑学习目标页面导航1. 声明式导航2. 编程式导航3. 导航传参 页面事件1. 下拉刷新2. 上拉触底3.扩展-自定义编译模式 生命周期1. 简介2. 生命周期函数3. 应用的生命周期函数4. 页面生命周期函数 WXS脚本1. 概述2. 基础语法3. WXS的特点4. 使用WXS处理手机号 总…

CVE-2020-26048(文件上传+SQL注入)

简介 CuppaCMS是一套内容管理系统&#xff08;CMS&#xff09;。 CuppaCMS 2019-11-12之前版本存在安全漏洞&#xff0c;攻击者可利用该漏洞在图像扩展内上传恶意文件&#xff0c;通过使用文件管理器提供的重命名函数的自定义请求&#xff0c;可以将图像扩展修改为PHP&#xf…

防火墙虚拟系统

防火墙虚拟系统 防火墙虚拟系统的应用场景 大中型企业的网络隔离 通过防火墙的虚拟系统将网络隔离为研发部门、财经部门和行政部门。各部门之间可以根据权限互相访问&#xff0c;不同部门的管理员权限区分明确。 云计算中心的安全网关 通过配置虚拟系统&#xff0c;可让部署…