08-基于互斥量的线程同步
13.2.2 基于互斥量的线程同步
QMutex和QMutexLocker是基于互斥量的线程同步类,QMutex定义的实例是一个互斥量,QMutex主要提供3个函数。
- lock():锁定互斥量,如果另外一个线程锁定了这个互斥量,它将阻塞执行直到其他线程解锁这个互斥量。
- unlock():解锁一个互斥量,需要与lock()配对使用。
- tryLock():试图锁定一个互斥量,如果成功锁定就返回true;如果其他线程已经锁定了这个互斥量,就返回false,但不阻塞程序执行。
使用互斥量,对QDiceThread类重新定义,不采用信号与槽机制,而是提供一个函数用于主线程读取数据。更改后的QDiceThread类定义如下:
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); //用于主线程读取数据的函数
};
QDiceThread类里用QMutex类定义了一个互斥量变量mutex。
定义了函数readValue(),用于外部线程读取掷骰子的次数和点数,传递参数采用指针变量,以便一次读取两个数据。
下面是QDiceThread类中关键的run()和readValue()函数的实现代码。
void QDiceThread::run()
{
m_stop=false;
m_seq=0;
qsrand(QTime::currentTime().msec());//随机数初始化
while(!m_stop)//循环主体
{
if (!m_paused)
{
mutex.lock();
m_diceValue=qrand(); //获取随机数
m_diceValue=(m_diceValue % 6)+1;
m_seq++;
mutex.unlock();
}
msleep(500); //线程休眠
}
}
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()和unlock()进行了保护,这部分代码的执行就不会被其他线程中断。注意,lock()与unlock()必须配对使用。
在readValue()函数中,用互斥量mutex的tryLock()和unlock()进行了保护。如果tryLock()成功锁定互斥量,读取数值的两行代码执行时不会被中断,执行完后解锁;如果tryLock()锁定失败,函数就立即返回,而不会等待。
原理上,对于两个或多个线程可能会同时读或写的变量应该使用互斥量进行保护,例如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_seq=0;
qsrand(QTime::currentTime().msec());//随机数初始化
while(!m_stop)//循环主体
{
if (!m_paused)
{
QMutexLocker Locker(&mutex);
m_diceValue=qrand(); //获取随机数
m_diceValue=(m_diceValue % 6)+1;
m_seq++;
}
msleep(500); //线程休眠
}
}
这样定义的QDiceThread类,在主程序中只能调用其readValue()函数来不断读取数值。实例samp13_2采用QMutex进行线程同步,实例samp13_3采用QMutex和QMutexLocker进行线程同步,其界面与samp13_1完全相同,只是增加了定时器,用于定时主动去读取掷骰子线程的数值。
实例程序samp13_2的Dialog类的主要定义如下(省略了一些系统生成的声明):
class Dialog : public QDialog
{
private:
int mSeq, mDiceValue;
QDiceThread threadA;
QTimer mTimer;//定时器
public:
explicit Dialog(QWidget *parent = 0);
private slots:
void onthreadA_started();
void onthreadA_finished();
void onTimeOut(); //定期器处理槽函数
};
主要是增加了一个定时器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);
}
}
窗口上几个按钮的代码如下(省略了按钮使能控制的代码):
void Dialog::on_btnStartThread_clicked()
{//启动线程
mSeq=0;
threadA.start();
}
void Dialog::on_btnStopThread_clicked()
{//结束线程
threadA.stopThread();//结束线程的run()函数执行
threadA.wait();
}
void Dialog::on_btnDiceBegin_clicked()
{//开始掷骰子
threadA.diceBegin();
mTimer.start(100); //定时器100ms读取一次数据
}
void Dialog::on_btnDiceEnd_clicked()
{//暂停掷骰子
threadA.diceEnd();
mTimer.stop();//定时器暂停
}
实例samp13_2和samp13_3实现的效果与实例samp13_1相同,只是实现的方式不同。