【C++】 Qt-线程并发与线程同步

2023-11-02

线程并发

我们再创建一个控制台文件命名为main2.cpp,然后在这个文件中创建三条线程,在这三条中同时为一个全局变量进行递增操作,并在最后输出这个全局变量。

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    //线程句柄
    HANDLE handle1 = ::CreateThread(nullptr/*默认的安全属性*/,
                   0/*windows 默认1M*/,
                   &ThreadProc/*线程函数的地址*/,
                   nullptr/*线程函数的参数*/,
                   0/*0:立即运行 ,SUS_PEND 挂起*/,
                   nullptr/*线程ID*/);

    HANDLE handle2 = ::CreateThread(nullptr/*默认的安全属性*/,
                   0/*windows 默认1M*/,
                   &ThreadProc/*线程函数的地址*/,
                   nullptr/*线程函数的参数*/,
                   0/*0:立即运行 ,SUS_PEND 挂起*/,
                   nullptr/*线程ID*/);

    HANDLE handle3 = ::CreateThread(nullptr/*默认的安全属性*/,
                   0/*windows 默认1M*/,
                   &ThreadProc/*线程函数的地址*/,
                   nullptr/*线程函数的参数*/,
                   0/*0:立即运行 ,SUS_PEND 挂起*/,
                   nullptr/*线程ID*/);

    if(WAIT_OBJECT_0 == WaitForSingleObject(handle1,INFINITE)){
        if(handle1){
            ::CloseHandle(handle1);
            handle1 = nullptr;
        }
    }

    if(WAIT_OBJECT_0 == WaitForSingleObject(handle2,INFINITE)){
        if(handle2){
            ::CloseHandle(handle2);
            handle2 = nullptr;
        }
    }

    if(WAIT_OBJECT_0 == WaitForSingleObject(handle3,INFINITE)){
        if(handle3){
            ::CloseHandle(handle3);
            handle3 = nullptr;
        }
    }
    qDebug()<<"N============== "<<N;
    return a.exec();
}

线程函数以及全局变量

int N = 0;
//线程函数
DWORD WINAPI ThreadProc (LPVOID lpThreadParameter){

    for(int i=0;i<100;i++){
        N++;
        qDebug()<<"N= "<<N;
        Sleep(50);
    }
    return 0;
}

我们经过多次测试发现,最终结果并不一定是我们预想的300

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ghGnBqj-1689425356251)(C++.assets/image-20230706045635228.png)]

这里出现的就是并发问题:

多个线程同时操作同一个资源(内存空间、文件句柄、网络句柄),可能会导致的结果不一致的问题。发生的前提条件一定是多个线程下共享资源。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IH7S7mWf-1689425356252)(C++.assets/image-20230706045839262.png)]

那么既然出现了问题,我们也会有相应的解决办法,就是接下来要说的线程同步

线程同步

线程同步,就是通过协调线程执行的顺序,避免多个线程同时操作同一个资源导致并发问题,使结果多次执行结果一致。

常见的线程同步方式:原子访问、关键段、事件、互斥量、条件变量、信号量。

上面提到的并发问题,解决方法有很多,重点学习 锁。

原子访问(InterLocked)

同一时刻,只允许一个线程访问一个变量。注意:他只是对一个变量保持原子自增、自减操作,对于一个代码段来说并不适用。

DWORD WINAPI ThreadProc (LPVOID lpThreadParameter){
    for(int i=0;i<100;i++){
        //N++;
        ::InterlockedIncrement((long*)&N);  //源自方式 递增 ++
        //N--;
        //::InterlockedDecrement((long*)&N);
        qDebug()<<"N= "<<N;
        Sleep(50);
    }
    return 0;
}

这样我们在每次运行都会得到理想的值了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Or8v2Qgy-1689425356252)(C++.assets/image-20230706050549533.png)]

关键段(Critical_Section,也叫临界区)

回顾单例出现的问题

我们还记得在学习 设计模式中的单例模式时,提到过一个问题,那就是在多线程下,可能会创建出来多个对象

我们试着把这个问题显现出来

单例:

class CSingleton {

    CSingleton(): m_a(0){}
    CSingleton(const CSingleton&) = delete;  //弃用 拷贝构造函数
    ~CSingleton(){}
    static CSingleton* m_psin;

    static class Destory {
    public:
        ~Destory() {
            if (m_psin) {
                delete m_psin;
            }
            m_psin = nullptr;
        }
    } m_destory;

public:
    //问题:在多线程下 可能会创建出来多个对象
    static CSingleton* GetSingleton() {
        //1.加锁
        if (!m_psin)
            m_psin = new CSingleton;
        //2.解锁
        return m_psin;
    }
    static void DestorySingleton(CSingleton*& psin) {
        if (m_psin) {
            delete m_psin;
        }
        psin = m_psin = nullptr;
    }

    int m_a;
};
CSingleton* CSingleton::m_psin = nullptr;

CSingleton::Destory CSingleton::m_destory;//类外定义

主函数

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    for(int i=0;i<30;i++){
        CreateThread(nullptr/*默认的安全属性*/,
                           0/*windows 默认1M*/,
                           &ThreadProc/*线程函数的地址*/,
                           nullptr/*线程函数的参数*/,
                           0/*0:立即运行 ,SUS_PEND 挂起*/,
                           nullptr/*线程ID*/);
    }
    return a.exec();
}

线程函数

DWORD WINAPI ThreadProc (LPVOID lpThreadParameter){
    Sleep(100);
    CSingleton* pSin = CSingleton::GetSingleton();
    qDebug()<<pSin;

    return 0;
}

运行后通过查看对象地址可以发现,真的可能会创建出多个对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GdKfrcet-1689425356252)(C++.assets/image-20230706052920751.png)]

那么这个问题就不能用原子访问来解决了,我们需要使用到关键段

关键段基本使用

结构体:CRITICAL_SECTION和四个函数(初始化、进入、离开、删除)

我们可以使用这个来解决单例中出现的问题

CRITICAL_SECTION m_cs;  //关键段的变量
    static CSingleton* GetSingleton() {
        //1.加锁
        if (!m_psin){
            ::EnterCriticalSection(&m_cs);
            if (!m_psin)
            m_psin = new CSingleton;
            //2.解锁
            ::LeaveCriticalSection(&m_cs);
        }
        return m_psin;
    }

主函数中初始化和删除,注意删除之前最好要加个延迟

    ::InitializeCriticalSection(&m_cs);  //初始化关键段

	......
        
    Sleep(3000);
    ::DeleteCriticalSection(&m_cs);  //删除关键段

这样在运行过程中就只会创建一个对象了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4MyuC6S9-1689425356253)(C++.assets/image-20230706055339117.png)]

运行原理:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XdASuiAx-1689425356253)(C++.assets/image-20230706055416621.png)]

封装关键段

把创建对象和四个结构体都封装到一个类中

class MyLock{
    CRITICAL_SECTION m_cs;  //关键段的变量
public:
    MyLock(){
        ::InitializeCriticalSection(&m_cs);  //初始化关键段
    }
    ~MyLock(){
        ::DeleteCriticalSection(&m_cs);  //删除关键段
    }
    void Lock(){
        ::EnterCriticalSection(&m_cs);
    }
    void UnLock(){
        ::LeaveCriticalSection(&m_cs);
    }
};

使用的时候直接创建类对象调用函数即可

static CSingleton* GetSingleton() {

    static MyLock lock;
    //1.加锁
    if (!m_psin){
        lock.Lock();
        if (!m_psin)
            m_psin = new CSingleton;
        //2.解锁
        lock.UnLock();
    }
    return m_psin;
}

Qt下的多线程

多线程与进度条

我们创建一个带有设计界面的项目,并且在设计界面中放入一个进度条以及四个按钮组件,组件的作用分别是启动、暂停、恢复和退出

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kZA18Ere-1689425356254)(C++.assets/image-20230707050229997.png)]

并将这些按钮直接转到clicked的槽函数

private slots:
    void on_pb_start_clicked();

    void on_pb_pause_clicked();

    void on_pb_resume_clicked();

    void on_pb_quit_clicked();
void MainWindow::on_pb_start_clicked()
{

}

void MainWindow::on_pb_pause_clicked()
{

}

void MainWindow::on_pb_resume_clicked()
{

}

void MainWindow::on_pb_quit_clicked()
{

}

在启动的槽函数中实现创建线程

void MainWindow::on_pb_start_clicked()
{

    if(!m_handle){
        m_isQuit = false;
        m_handle = CreateThread(nullptr/*默认的安全属性*/,
                           0/*windows 默认1M*/,
                           &ThreadProc/*线程函数的地址*/,
                           (void*)this/*线程函数的参数*/,
                           0/*0:立即运行 ,SUS_PEND 挂起*/,
                           nullptr/*线程ID*/);
    }

}

线程函数,在线程函数中实现设置进度条的值

DWORD WINAPI ThreadProc (LPVOID lpThreadParameter){

    MainWindow* pMain = (MainWindow*)lpThreadParameter;
    int n = 0;

    while(!pMain->m_isQuit){
        pMain->GetUI()->progressBar->setValue(n);  //设置进度条的值

        n = ++n%101;
        Sleep(200);
    }
    return 0;
}

在头文件中设置公有的ui接口以及退出标志和线程句柄

public:
    Ui::MainWindow * GetUI(){return ui;}

    bool m_isQuit;  //是否退出
    HANDLE m_handle;

在构造函数中初始化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4A6jruyn-1689425356254)(C++.assets/image-20230707050654263.png)]

到这里我们先点击启动按钮进行测试,发现程序 会崩溃

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I3Akl0z2-1689425356254)(C++.assets/image-20230707051136480.png)]

报错原因是:在另一个线程中设置了当前组件的值时,使用sendEvent发送事件,跨线程报错。

针对于此情况解决方法就是手动设置信号与槽进行连接

所以我们先创建一个自定义槽函数和信号

private slots:
    void slots_setValue(int);

signals:
    void signals_sendValue(int);

这样我们就在线程函数中去发射信号,然后在槽函数中接收信号并且设置进度条的值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dGmmFxFW-1689425356255)(C++.assets/image-20230707053100143.png)]

槽函数

void MainWindow::slots_setValue(int n){
    ui->progressBar->setValue(n);  //设置进度条的值
}

之后对信号和槽进行连接

    connect(this,SIGNAL(signals_sendValue(int)),this,SLOT(slots_setValue(int)),Qt::QueuedConnection);

注意这个函数有有一个第五个参数,用来设置连接类型,我们之前报错只要就是因为默认的连接类型不符合,我们可以选择AutoConnection或QueuedConnection。

这样程序就可以正常运行了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SjiGfc1R-1689425356255)(C++.assets/image-20230707053351443.png)]

然后我们再来实现以下退出槽函数

void MainWindow::on_pb_quit_clicked()
{
    m_isQuit = true;  //通知线程退出

    if( WAIT_OBJECT_0 == WaitForSingleObject(m_handle,3000)){
        qDebug()<<"子线程已经退出";
        if(m_handle){
            CloseHandle(m_handle);
            m_handle = nullptr;
        }
    }
}

暂停和恢复就是将线程挂起和连接

暂停

void MainWindow::on_pb_pause_clicked()
{
    ::SuspendThread(m_handle);
    qDebug()<<"子线程已经挂起";
}

恢复

void MainWindow::on_pb_resume_clicked()
{
    ::ResumeThread(m_handle);
    qDebug()<<"子线程已经恢复";

}

通过测试都可以正常运行

Qt-QThread

在Qt中封装了创建线程的类:QThread,一般情况下,手动添加一个类继承QThread,这样既能继承QThread的功能,又能扩展自己的功能。

添加新文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fzZ3zazI-1689425356255)(C++.assets/image-20230711205938295.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EsNZMdwJ-1689425356255)(C++.assets/image-20230711210119334.png)]

Qt线程+关键段:为何要使用关键段,原来的只是靠一个暂停标志位,while一直在空跑,也会耗费一点cpu。好一点的效果是让其等待,直到恢复。

所以我们就会创建一个新的线程用来阻塞当前线程

头文件

#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QThread>
#include <windows.h>

class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = 0);
    ~MyThread();

signals:
    void signals_sendThreadValue(int);  //声明信号

public slots:

public:
    virtual void run();

    bool m_isQuit;  //标识当前线程,是否退出
    bool m_isPause;  //是否暂停

    CRITICAL_SECTION m_cs;
};

#endif // MYTHREAD_H

源文件

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

MyThread::MyThread(QObject *parent) : QThread(parent),m_isQuit(false),m_isPause(false)
{
    ::InitializeCriticalSection(&m_cs);
}
MyThread::~MyThread(){
    ::DeleteCriticalSection(&m_cs);

}

void MyThread::run(){
    int n = 0;
    while(!m_isQuit){

        if(m_isPause){
//            qDebug()<<"暂停了";
//            continue;
            qDebug()<<"暂停了";
            ::EnterCriticalSection(&m_cs);
            qDebug()<<"进入关键段";
            ::LeaveCriticalSection(&m_cs);
            qDebug()<<"离开关键段";

        }

        //发射一个信号
        emit signals_sendThreadValue(n);

        n = ++n%101;
        Sleep(100);
    }
    qDebug()<<"子线程即将退出";
}

信号和槽连接以及实现线程功能

    connect(&this->m_thread,SIGNAL(signals_sendThreadValue(int)),this,SLOT(slots_setValue(int)),Qt::QueuedConnection);

启动线程

void MainWindow::on_pb_start_clicked()
{
    m_thread.m_isQuit = false;
    m_thread.start();  //启动线程
}

注意:启动线程需要调用系统函数start()而不是直接调用run()。

暂停线程

void MainWindow::on_pb_pause_clicked()
{
    ::EnterCriticalSection(&m_thread.m_cs);
    m_thread.m_isPause = true;
}

恢复线程

void MainWindow::on_pb_resume_clicked()
{
    m_thread.m_isPause = false;
    ::LeaveCriticalSection(&m_thread.m_cs);
}

退出线程

void MainWindow::on_pb_quit_clicked()
{
    m_thread.m_isQuit = true;
}

运行测试后也没有什么毛病

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U6mPdhBq-1689425356255)(C++.assets/image-20230711214629735.png)]

到此,线程相关的知识就结束了

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C++】 Qt-线程并发与线程同步 的相关文章

随机推荐

  • bootstrap table 表格支持shirt 多选_bootstrap-table 表格行内编辑实现

    这篇文章向大家介绍一下如何使用bootstrap table插件实现表格的行内编辑功能 我的web前端学习交流群点击进入1045267283 欢迎加入 先放一张效果图 应用场景 之前的项目也是采用bootstrap table 添加和修改数
  • 牛客——子序列(组合数学)

    子序列 题目描述 给定一个小写字母字符串 T T T 求有多少长度为 m m m的小写字母字符串 S
  • 端口相关知识总结

    端口相关知识总结 80是服务器上的一个软件 服务器软件 端口是软件的代号 3306是MySQL的端口 1521是Oracle的端口 80是外部服务器的通用端口 京东也是 不写也可以访问 80端口可以省略 文件下载端口 FTP 都是21 FT
  • Android 13 - Media框架(2)- Demo App与MediaPlayer Api了解

    尝试用MediaPlayer写了一个播放demo 实现了网络流和本地流的播放 由于本人对app开发一窍不通 所以demo中很多内容是边查资料边写的 写的也比较杂乱 能够帮助理解api就行 这一节主要会记录demo开发中学到的内容 以及了解M
  • LeetCode 312. Burst Balloons(戳气球)

    原题网址 https leetcode com problems burst balloons Given n balloons indexed from 0 to n 1 Each balloon is painted with a nu
  • 微信小程序云开发实现微信小程序订阅消息服务通知教程

    微信小程序云开发实现微信小程序订阅消息服务通知教程 申请模板 云函数 小程序页面 调试 我这里就直接真机测试了 申请模板 1 在这边菜单栏 找到 功能 里的 订阅消息 2 在 公共模板库 里面选取自己想要的模板 选取自己想要的消息即可 云函
  • 『学Vue2+Vue3』指令补充、computed计算属性、watch侦听器

    day02 一 今日学习目标 1 指令补充 指令修饰符 v bind对样式增强的操作 v model应用于其他表单元素 2 computed计算属性 基础语法 计算属性vs方法 计算属性的完整写法 成绩案例 3 watch侦听器 基础写法
  • linux文件基础-2_linux文件细节_lseek_文件指针

    一 linux管理文件 1 硬盘中的静态文件和inode i节点 1 静态文件 放在硬盘中 固定的形式 2 硬盘的两大区域 1 硬盘内容管理表项和储存内容区域 2 操作系统先去访问硬盘内容管理表项 gt 扇区级别的信息 gt 得到储存内容区
  • SAP 货币类型和公司代码的货币设置

    货币类型分为公司代码和集团货币 一般FI 10类型和集团货币 30 事务代码是8KEM 设置货币类型的事务代码是OB22 在S 4 1809版本里编辑功能统合到事务代码FINSC LEDGER 中了 这里集中了分类账和公司代码的设置 设置多
  • 使用正确的命令重启WSL子系统

    问题 大家都知道一般Linux系统重启非常简单 但是在WSL子系统中执行以下两个重启命令是完全无效的 reboot shutdown r 执行命令后提示如下 System has not been booted with systemd a
  • JavaWeb——Servlet

    目录 一 JavaWeb 二 servlet本质 三 Servlet对象生命周期 四 Servlet类的方法介绍 五 适配器思想 一 JavaWeb 对于一个web应用来说 涉及到的角色和规范协议 二 servlet本质 可以将servle
  • Over-smoothing issue in graph neural network(GNN中的过平滑问题)

    在这里转载只是为了让不能够科学搜索的同学们看到好文章而已 个人无收益只是分享知识 顺手做个记录罢了 原网址 https towardsdatascience com over smoothing issue in graph neural
  • 2011.11.24

    完成了刚体 并基本上封装好了
  • 小程序文章详情界面id传送问题

    今天在做由文章列表跳转至文章详情界面时发现不能正常获取文章ID 控制台显示未定义 经过询问他人与搜索资料终于找到了问题所在之处 心累 可以看到这里显示id未定义 错误中学到了什么 大家在发现错误时 一定要善于用console log 来看一
  • 解决创建Vue项目出现template下方有红色波浪错误

    问题 在创建完vue项目后每个点开的文件只要有template或const等单词都会出现红色波浪线报错提示 虽然不影响项目运行 但是看着还是非常碍眼 将鼠标一上去会显示 No Babel config file detected for 路
  • Linux中top命令参数详解

    因为面试经常会问top命令用法 以及各个参数的含义 因此转载补充了了一下 以便自己学习 top命令经常用来监控linux的系统状况 是常用的性能分析工具 能够实时显示系统中各个进程的资源占用情况 top的使用方式 top d number
  • 小程序引入vant-Weapp保姆级教程及安装过程的问题解决

    小知识 大挑战 本文正在参与 程序员必备小知识 创作活动 本文同时参与 掘力星计划 赢取创作大礼包 挑战创作激励金 当你想在小程序里引入vant时 第一步 打开官方文档 第二步 切到快速上手 然后开始步骤一 步骤二 步骤三 你只会看到 以下
  • Awesome IoT

    本文来自 https github com HQarroum awesome iot 中文可以参考 https yq aliyun com articles 54793 Inspired by the awesome list thing
  • 基于BowyerWatson的Delaunay三角化算法实现

    实现效果如下图所示 代码 include
  • 【C++】 Qt-线程并发与线程同步

    文章目录 线程并发 线程同步 原子访问 InterLocked 关键段 Critical Section 也叫临界区 回顾单例出现的问题 关键段基本使用 封装关键段 Qt下的多线程 多线程与进度条 Qt QThread 线程并发 我们再创建