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

线程并发

我们再创建一个控制台文件命名为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)]

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

线程同步

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

扫描二维码关注公众号,回复: 16751093 查看本文章

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

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

原子访问(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)]

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

猜你喜欢

转载自blog.csdn.net/jia_03/article/details/131743941
今日推荐