13-2_Qt 5.9 C++开发指南_线程同步_QMutex+QMutexLocker(目前较为常用)

文章目录

  • 1.线程同步的概念
  • 2. 基于互斥量的线程同步
  • 3.QMutex实现线程同步源代码
    • 3.1 qdicethread.h
    • 3.2 qdicethread.cpp
    • 3.3 dialog.h
    • 3.4 dialog.cpp
  • 4.QMutexLocker 实现线程同步源代码
    • 4.1 qdicethread.h
    • 4.2 qdicethread.cpp
    • 4.3 dialog.h
    • 4.4 dialog.cpp

1.线程同步的概念

在多线程应用程序中,由于多个线程的存在,线程之间可能需要访问同一个变量,或一个线程需要等待另外一个线程完成某个操作后才产生相应的动作。例如,在上一节的实例 samp13_1 中工作线程产生随机的骰子点数,主线程读取散子点数并显示,主线程需要等待工作线程产生一个新的骰子点数后再读取数据。实例 samp13_1 中使用了信号与槽的机制,在产生新的子数之后通过信号通知主线程读取新的数据。

如果不使用信号与槽机制,QDiceThread 的run()函数变为如下的代码:

void QDiceThread::run()
{
    m_stop=false;//启动线程时令m_stop=false
    m_seq=0;
    qsrand(QTime::currentTime().msec());//随机数初始化,qsrand是线程安全的

    while(!m_stop)//循环主体
    {
        if (!m_paused)
        {
            mutex.lock();
            m_diceValue=qrand(); //获取随机数
            m_diceValue=(m_diceValue % 6)+1;
            m_seq++;
            mutex.unlock();
        }
        msleep(500); //线程休眠100ms
    }
}

那么,QDiceThread 需要定义公共函数,返回m_diceValue 的值,如:

int QDiceThread::diceValue () {return = m diceValue};

以便在主线程中调用此函数读取骰子的点数。

由于没有信号与槽的关联(信号与槽的关系类似于硬件的中断与中断处理函数),主线程只能采用不断查询的方式主动查询是否有新数据,并读取它。但是在主线程调用 diceValue()读取散子点数时,工作线程可能正在执行 run()函数里修改 m_diceValue 值的语句,即:

            m_diceValue=qrand(); //获取随机数
            m_diceValue=(m_diceValue % 6)+1;
            m_seq++;

而且这几条语句计算量大,需要执行较长时间。执行这两条语句时不希望被主线程调用的diceValue()函数中断,如果中断,则主线程得到的可能是错误的值。

这种情况下,这样的代码段是希望被保护起来的,在执行过程中不能被其他线程打断,以保证计算结果的完整性,这就是线程同步的概念。

在Qt中,有多个类可以实现线程同步的功能,包括 QMutex、QMutexLocker、QReadWriteLock、QReadLocker、QWriteLocker、QWaitCondition 和QSemaphore。下面将分别介绍这些类的用法。

2. 基于互斥量的线程同步

QMutex和QMutexLocker 是基于互斥量的线程同步类,QMutex定义的实例是个互斥量,QMutex主要提供3个函数。

  • lock():锁定互斥量,如果另外一个线程锁定了这个互斥量,它将阻塞执行直到其他线程解锁这个互斥量。
  • unlock():解锁一个互斥量,需要与 lock()配对使用。
  • tryLock():试图锁定一个互斥量,如果成功锁定就返回 true; 如果其他线程已经锁定了这个互斥量,就返回 false,但不阻塞程序执行。

使用互斥量,对 QDiceThread 类重新定义,不采用信号与槽机制,而是提供一个函数用于主线程读取数据。更改后的 QDiceThread 类定义如下:

#ifndef QDICETHREAD_H
#define QDICETHREAD_H

//#include    
#include    
#include    

class QDiceThread : public QThread
{
    Q_OBJECT

private:
    QMutex  mutex; //互斥量

    int     m_seq=0;//序号
    int     m_diceValue;
    bool    m_paused=true;
    bool    m_stop=false; //停止线程
protected:
    void    run() Q_DECL_OVERRIDE;
public:
    QDiceThread();

    void    diceBegin();//掷一次骰子
    void    diceEnd();//
    void    stopThread();

    bool    readValue(int *seq, int *diceValue); //用于主线程读取数据的函数
};

#endif // QDICETHREAD_H

QDiceThread 类里用QMutex 类定义了一个互斥量变量 mutex。

定义了函数 readValue(),用于外部线程读取掷骰子的次数和点数,传递参数采用指针变量以便一次读取两个数据。
下面是QDiceThread 类中关键的 run()和readValue()函数的实现代码。

void QDiceThread::run()
{
    m_stop=false;//启动线程时令m_stop=false
    m_seq=0;
    qsrand(QTime::currentTime().msec());//随机数初始化,qsrand是线程安全的

    while(!m_stop)//循环主体
    {
        if (!m_paused)
        {
            mutex.lock();
            m_diceValue=qrand(); //获取随机数
            m_diceValue=(m_diceValue % 6)+1;
            m_seq++;
            mutex.unlock();
        }
        msleep(500); //线程休眠100ms
    }
}
bool QDiceThread::readValue(int *seq, int *diceValue)
{
    if (mutex.tryLock())
    {
        *seq=m_seq;
        *diceValue=m_diceValue;
        mutex.unlock();
        return true;
    }
    else
        return false;
}

在 run()函数中,对重新计算骰子点数和掷骰子次数的 3 行代码用互斥量 mutex 的 lock()和unlock0()进行了保护,这部分代码的执行就不会被其他线程中断。注意,lock()与 unlock()必须配对使用。

在 readValue()函数中,用互斥量 mutex 的 tryLock()和 unlock()进行了保护。如果 tryLock()成功锁定互斥量,读取数值的两行代码执行时不会被中断,执行完后解锁;如果 tyLock()锁定失败函数就立即返回,而不会等待。

原理上,对于两个或多个线程可能会同时读或写的变量应该使用互斥量进行保护,例如QDiceThread 中的变量 m_stop 和 m_paused,在 run()函数中读取这两个变量,要在 diceBegin()、diceEnd()和 stopThread()函数里修改这些值,但是这 3 个函数都只有一条赋值语句,可以认为是原子操作,所以,可以不用锁定保护(这种说法应该放到汇编语言的角度去看)。

定义的互斥量 mutex 相当于一个标牌,可以这样来理解互斥量:列车上的卫生间一次只能进一个人,当一个人尝试进入卫生间就是 lock(),如果有人占用,他就只能等待;等里面的人出来,腾出了卫生间是 unlock(),这个等待的人才可以进入并且锁住卫生间的门,就是 lock(),使用完卫生间之后他再出来时就是 unlock()。

QMutex 需要配对使用 lock()和 unlock()来实现代码段的保护,在一些逻辑复杂的代码段或可能发生异常的代码中,配对就可能出错。

QMutexLocker 是另外个简化了互斥量处理的类。QMutexLocker的构造函数接受一个互斥量作为参数并将其锁定,QMutexLocker 的析构函数则将此互斥量解锁,所以在 QMutexLocker 实例变量的生存期内的代码段得到保护,自动进行互斥量的锁定和解锁。例如,QDiceThread 的 run()函数的代码可以改写如下:

void QDiceThread::run()
{
    m_stop=false;//启动线程时令m_stop=false
    m_seq=0;
    qsrand(QTime::currentTime().msec());//随机数初始化,qsrand是线程安全的

    while(!m_stop)//循环主体
    {
        if (!m_paused)
        {
//            mutex.lock();
            QMutexLocker    Locker(&mutex);
            m_diceValue=qrand(); //获取随机数
            m_diceValue=(m_diceValue % 6)+1;
            m_seq++;
//            mutex.unlock();
        }
        msleep(500); //线程休眠100ms
    }
}

这样定义的 QDiceThread 类,在主程序中只能调用其 readValue()函数来不断读取数值。实例samp13_2采用QMutex 进行线程同步,实例 sampl3_3 采用QMutex和qMutexLocker 进行线程同步,其界面与 samp13_1 完全相同,只是增加了定时器,用于定时主动去读取掷骰子线程的数值。实例程序 samp13_2的 Dialog 类的主要定义如下(省略了一些系统生成的声明):

#ifndef DIALOG_H
#define DIALOG_H

#include    
#include    

#include    "qdicethread.h"

namespace Ui {
class Dialog;
}

class Dialog : public QDialog
{
    Q_OBJECT

private:
    int mSeq,mDiceValue;

    QDiceThread   threadA;
    QTimer  mTimer;//定时器
protected:
    void    closeEvent(QCloseEvent *event);
public:
    explicit Dialog(QWidget *parent = 0);
    ~Dialog();

private slots:
    void    onthreadA_started();
    void    onthreadA_finished();

    void    onTimeOut(); //定期器处理槽函数
...

private:
    Ui::Dialog *ui;
};

#endif // DIALOG_H

主要是增加了一个定时器 mTimer 和其时间溢出响应槽函数 onTimeOut(),在 Dialog 的构造函数中将mTimer的timeout 信号与此槽函数关联。

connect(&mTimer,SIGNAL(timeout()),this,SLOT(onTimeOut()));

onTimeOut()函数的主要功能是调用 threadA 的readValue()函数读取数值。定时器的定时周期设置为 100ms,小于 threadA 产生一次新数据的周期(500ms),所以可能读出旧的数据,通过存储的掷骰子的次数与读取的掷散子次数是否不同,判断是否为新数据。onTimeOut()函数的代码如下:

void Dialog::onTimeOut()
{ //定时器到时处理槽函数
    int tmpSeq=0,tmpValue=0;
    bool  valid=threadA.readValue(&tmpSeq,&tmpValue); //读取数值
    if (valid && (tmpSeq!=mSeq)) //有效,并且是新数据
    {
        mSeq=tmpSeq;
        mDiceValue=tmpValue;
        QString  str=QString::asprintf("第 %d 次掷骰子,点数为:%d",mSeq,mDiceValue);
        ui->plainTextEdit->appendPlainText(str);
        QPixmap pic;
        QString filename=QString::asprintf(":/dice/images/d%d.jpg",mDiceValue);
        pic.load(filename);
        ui->LabPic->setPixmap(pic);
    }
}

窗口中的几个按钮的代码起到的作用也是类似的,这里不细讲。

3.QMutex实现线程同步源代码

3.1 qdicethread.h

#ifndef QDICETHREAD_H
#define QDICETHREAD_H

//#include    
#include    
#include    

class QDiceThread : public QThread
{
    Q_OBJECT

private:
    QMutex  mutex; //互斥量

    int     m_seq=0;//序号
    int     m_diceValue;
    bool    m_paused=true;
    bool    m_stop=false; //停止线程
protected:
    void    run() Q_DECL_OVERRIDE;
public:
    QDiceThread();

    void    diceBegin();//掷一次骰子
    void    diceEnd();//
    void    stopThread();

    bool    readValue(int *seq, int *diceValue); //用于主线程读取数据的函数
};

#endif // QDICETHREAD_H

3.2 qdicethread.cpp

#include "qdicethread.h"
#include    

QDiceThread::QDiceThread()
{

}

void QDiceThread::diceBegin()
{
//    mutex.lock();
    m_paused=false;
//    mutex.unlock();
}

void QDiceThread::diceEnd()
{
//    mutex.lock();
    m_paused=true;
//    mutex.unlock();
}

void QDiceThread::stopThread()
{
//    mutex.lock();
    m_stop=true;
//    mutex.unlock();
}

bool QDiceThread::readValue(int *seq, int *diceValue)
{
    if (mutex.tryLock())
    {
        *seq=m_seq;
        *diceValue=m_diceValue;
        mutex.unlock();
        return true;
    }
    else
        return false;
}

void QDiceThread::run()
{
    m_stop=false;//启动线程时令m_stop=false
    m_seq=0;
    qsrand(QTime::currentTime().msec());//随机数初始化,qsrand是线程安全的

    while(!m_stop)//循环主体
    {
        if (!m_paused)
        {
            mutex.lock();
            m_diceValue=qrand(); //获取随机数
            m_diceValue=(m_diceValue % 6)+1;
            m_seq++;
            mutex.unlock();
        }
        msleep(500); //线程休眠100ms
    }
}

3.3 dialog.h

#ifndef DIALOG_H
#define DIALOG_H

#include    
#include    

#include    "qdicethread.h"

namespace Ui {
class Dialog;
}

class Dialog : public QDialog
{
    Q_OBJECT

private:
    int mSeq,mDiceValue;

    QDiceThread   threadA;
    QTimer  mTimer;//定时器
protected:
    void    closeEvent(QCloseEvent *event);
public:
    explicit Dialog(QWidget *parent = 0);
    ~Dialog();

private slots:
    void    onthreadA_started();
    void    onthreadA_finished();

    void    onTimeOut(); //定期器处理槽函数

    void on_btnClear_clicked();

    void on_btnDiceEnd_clicked();

    void on_btnDiceBegin_clicked();

    void on_btnStopThread_clicked();

    void on_btnStartThread_clicked();

private:
    Ui::Dialog *ui;
};

#endif // DIALOG_H

3.4 dialog.cpp

#include "dialog.h"
#include "ui_dialog.h"

void Dialog::closeEvent(QCloseEvent *event)
{//窗口关闭时
    if (threadA.isRunning())
    {
        threadA.stopThread();
        threadA.wait();
    }
    event->accept();
}

Dialog::Dialog(QWidget *parent) :    QDialog(parent),    ui(new Ui::Dialog)
{//构造函数
    ui->setupUi(this);

    connect(&threadA,SIGNAL(started()),this,SLOT(onthreadA_started()));
    connect(&threadA,SIGNAL(finished()),this,SLOT(onthreadA_finished()));

    connect(&mTimer,SIGNAL(timeout()),this,SLOT(onTimeOut()));
}

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

void Dialog::onthreadA_started()
{
    ui->LabA->setText("Thread状态:thread started");
}

void Dialog::onthreadA_finished()
{
    ui->LabA->setText("Thread状态:thread finished");
}

void Dialog::onTimeOut()
{ //定时器到时处理槽函数
    int tmpSeq=0,tmpValue=0;
    bool  valid=threadA.readValue(&tmpSeq,&tmpValue); //读取数值
    if (valid && (tmpSeq!=mSeq)) //有效,并且是新数据
    {
        mSeq=tmpSeq;
        mDiceValue=tmpValue;
        QString  str=QString::asprintf("第 %d 次掷骰子,点数为:%d",mSeq,mDiceValue);
        ui->plainTextEdit->appendPlainText(str);
        QPixmap pic;
        QString filename=QString::asprintf(":/dice/images/d%d.jpg",mDiceValue);
        pic.load(filename);
        ui->LabPic->setPixmap(pic);
    }
}

void Dialog::on_btnClear_clicked()
{//清空文本
    ui->plainTextEdit->clear();
}

void Dialog::on_btnDiceEnd_clicked()
{//暂停掷骰子
    threadA.diceEnd(); //
    mTimer.stop();//定时器暂停

    ui->btnDiceBegin->setEnabled(true);
    ui->btnDiceEnd->setEnabled(false);
}

void Dialog::on_btnDiceBegin_clicked()
{//开始掷骰子
    threadA.diceBegin();
    mTimer.start(100); //定时器100读取一次数据
    ui->btnDiceBegin->setEnabled(false);
    ui->btnDiceEnd->setEnabled(true);
}

void Dialog::on_btnStopThread_clicked()
{//结束线程
    threadA.stopThread();//结束线程的run()函数执行
    threadA.wait();//

    ui->btnStartThread->setEnabled(true);
    ui->btnStopThread->setEnabled(false);

    ui->btnDiceBegin->setEnabled(false);
    ui->btnDiceEnd->setEnabled(false);
}

void Dialog::on_btnStartThread_clicked()
{//启动线程
    mSeq=0;
    threadA.start();

    ui->btnStartThread->setEnabled(false);
    ui->btnStopThread->setEnabled(true);

    ui->btnDiceBegin->setEnabled(true);
    ui->btnDiceEnd->setEnabled(false);
}

4.QMutexLocker 实现线程同步源代码

4.1 qdicethread.h

#ifndef QDICETHREAD_H
#define QDICETHREAD_H

//#include    
#include    
#include    

class QDiceThread : public QThread
{
    Q_OBJECT

private:
    QMutex  mutex; //互斥量

    int     m_seq=0;//序号
    int     m_diceValue;
    bool    m_paused=true;
    bool    m_stop=false; //停止线程
protected:
    void    run() Q_DECL_OVERRIDE;
public:
    QDiceThread();

    void    diceBegin();//掷一次骰子
    void    diceEnd();//
    void    stopThread();

    bool    readValue(int *seq, int *diceValue); //用于主线程读取数据的函数
};

#endif // QDICETHREAD_H

4.2 qdicethread.cpp

#include "qdicethread.h"
#include    

QDiceThread::QDiceThread()
{

}

void QDiceThread::diceBegin()
{
//    mutex.lock();
    m_paused=false;
//    mutex.unlock();
}

void QDiceThread::diceEnd()
{
//    mutex.lock();
    m_paused=true;
//    mutex.unlock();
}

void QDiceThread::stopThread()
{
//    mutex.lock();
    m_stop=true;
//    mutex.unlock();
}

bool QDiceThread::readValue(int *seq, int *diceValue)
{
    if (mutex.tryLock())
    {
        *seq=m_seq;
        *diceValue=m_diceValue;
        mutex.unlock();
        return true;
    }
    else
        return false;
}

void QDiceThread::run()
{
    m_stop=false;//启动线程时令m_stop=false
    m_seq=0;
    qsrand(QTime::currentTime().msec());//随机数初始化,qsrand是线程安全的

    while(!m_stop)//循环主体
    {
        if (!m_paused)
        {
//            mutex.lock();
            QMutexLocker    Locker(&mutex);
            m_diceValue=qrand(); //获取随机数
            m_diceValue=(m_diceValue % 6)+1;
            m_seq++;
//            mutex.unlock();
        }
        msleep(500); //线程休眠100ms
    }
}


4.3 dialog.h

#ifndef DIALOG_H
#define DIALOG_H

#include    
#include    

#include    "qdicethread.h"

namespace Ui {
class Dialog;
}

class Dialog : public QDialog
{
    Q_OBJECT

private:
    int mSeq,mDiceValue;

    QDiceThread   threadA;
    QTimer  mTimer;//定时器
protected:
    void    closeEvent(QCloseEvent *event);
public:
    explicit Dialog(QWidget *parent = 0);
    ~Dialog();

private slots:
    void    onthreadA_started();
    void    onthreadA_finished();

    void    onTimeOut(); //定期器处理槽函数

    void on_btnClear_clicked();

    void on_btnDiceEnd_clicked();

    void on_btnDiceBegin_clicked();

    void on_btnStopThread_clicked();

    void on_btnStartThread_clicked();

private:
    Ui::Dialog *ui;
};

#endif // DIALOG_H

4.4 dialog.cpp

#include "dialog.h"
#include "ui_dialog.h"

void Dialog::closeEvent(QCloseEvent *event)
{//窗口关闭时
    if (threadA.isRunning())
    {
        threadA.stopThread();
        threadA.wait();
    }
    event->accept();
}

Dialog::Dialog(QWidget *parent) :    QDialog(parent),    ui(new Ui::Dialog)
{//构造函数
    ui->setupUi(this);

    connect(&threadA,SIGNAL(started()),this,SLOT(onthreadA_started()));
    connect(&threadA,SIGNAL(finished()),this,SLOT(onthreadA_finished()));

    connect(&mTimer,SIGNAL(timeout()),this,SLOT(onTimeOut()));
}

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

void Dialog::onthreadA_started()
{
    ui->LabA->setText("Thread状态:thread started");
}

void Dialog::onthreadA_finished()
{
    ui->LabA->setText("Thread状态:thread finished");
}

void Dialog::onTimeOut()
{ //定时器到时处理槽函数
    int tmpSeq=0,tmpValue=0;
    bool  valid=threadA.readValue(&tmpSeq,&tmpValue); //读取数值
    if (valid && (tmpSeq!=mSeq)) //有效,并且是新数据
    {
        mSeq=tmpSeq;
        mDiceValue=tmpValue;
        QString  str=QString::asprintf("第 %d 次掷骰子,点数为:%d",mSeq,mDiceValue);
        ui->plainTextEdit->appendPlainText(str);
        QPixmap pic;
        QString filename=QString::asprintf(":/dice/images/d%d.jpg",mDiceValue);
        pic.load(filename);
        ui->LabPic->setPixmap(pic);
    }
}

void Dialog::on_btnClear_clicked()
{//清空文本
    ui->plainTextEdit->clear();
}

void Dialog::on_btnDiceEnd_clicked()
{//暂停掷骰子
    threadA.diceEnd(); //
    mTimer.stop();//定时器暂停

    ui->btnDiceBegin->setEnabled(true);
    ui->btnDiceEnd->setEnabled(false);
}

void Dialog::on_btnDiceBegin_clicked()
{//开始掷骰子
    threadA.diceBegin();
    mTimer.start(100); //定时器100读取一次数据
    ui->btnDiceBegin->setEnabled(false);
    ui->btnDiceEnd->setEnabled(true);
}

void Dialog::on_btnStopThread_clicked()
{//结束线程
    threadA.stopThread();//结束线程的run()函数执行
    threadA.wait();//

    ui->btnStartThread->setEnabled(true);
    ui->btnStopThread->setEnabled(false);

    ui->btnDiceBegin->setEnabled(false);
    ui->btnDiceEnd->setEnabled(false);
}

void Dialog::on_btnStartThread_clicked()
{//启动线程
    mSeq=0;
    threadA.start();

    ui->btnStartThread->setEnabled(false);
    ui->btnStopThread->setEnabled(true);

    ui->btnDiceBegin->setEnabled(true);
    ui->btnDiceEnd->setEnabled(false);
}

你可能感兴趣的:(#,Qt,5.9,C++开发指南,qt,c++)