文章目录
- 线程并发
 - 线程同步
 - 原子访问(InterLocked)
 - 关键段(Critical_Section,也叫临界区)
 - 回顾单例出现的问题
 - 关键段基本使用
 - 封装关键段
 
- Qt下的多线程
 - 多线程与进度条
 - Qt-QThread
 
线程并发
我们再创建一个控制台文件命名为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)]](https://img-blog.csdnimg.cn/b54753e260c24e6cb44427cef3e50f7e.png)
这里出现的就是并发问题:
多个线程同时操作同一个资源(内存空间、文件句柄、网络句柄),可能会导致的结果不一致的问题。发生的前提条件一定是多个线程下共享资源。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IH7S7mWf-1689425356252)(C++.assets/image-20230706045839262.png)]](https://img-blog.csdnimg.cn/86713e42c9a04809b8f6e98bdecfc25d.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)]](https://img-blog.csdnimg.cn/1d9171ac8e384d7fbd1fd248a5a6af0d.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)]](https://img-blog.csdnimg.cn/21fd88a81bf94933b7be99d74f3368cd.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)]](https://img-blog.csdnimg.cn/47a56cfa229c4e25bebfba4614fc1090.png)
运行原理:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XdASuiAx-1689425356253)(C++.assets/image-20230706055416621.png)]](https://img-blog.csdnimg.cn/acb5b454ead34af08878d64c1fbda526.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)]](https://img-blog.csdnimg.cn/d653620682da435f90be3ed3c399fa01.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)]](https://img-blog.csdnimg.cn/7be1bed1a916451e81a234ab97a860ba.png)
到这里我们先点击启动按钮进行测试,发现程序 会崩溃
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I3Akl0z2-1689425356254)(C++.assets/image-20230707051136480.png)]](https://img-blog.csdnimg.cn/a208ab201e99427a8c15a4bea3b06f12.png)
报错原因是:在另一个线程中设置了当前组件的值时,使用sendEvent发送事件,跨线程报错。
针对于此情况解决方法就是手动设置信号与槽进行连接
所以我们先创建一个自定义槽函数和信号
private slots:
    void slots_setValue(int);
signals:
    void signals_sendValue(int);
 
这样我们就在线程函数中去发射信号,然后在槽函数中接收信号并且设置进度条的值
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dGmmFxFW-1689425356255)(C++.assets/image-20230707053100143.png)]](https://img-blog.csdnimg.cn/0b37b2f8043f4dfd8cc48463682c1fec.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)]](https://img-blog.csdnimg.cn/0fed83f88cf04df297a0f6d58c66c445.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)]](https://img-blog.csdnimg.cn/af65254885f744baaa2266d68b01ff23.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EsNZMdwJ-1689425356255)(C++.assets/image-20230711210119334.png)]](https://img-blog.csdnimg.cn/c73fcfde8b394f8a83da64b4f887853e.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)]](https://img-blog.csdnimg.cn/5c9195277856450e9d0d470eccb2f682.png)
到此,线程相关的知识就结束了

















