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)
{
m_diceValue=qrand(); //获取随机数
m_diceValue=(m_diceValue % 6)+1;
m_seq++;
//emit newValue(m_seq,m_diceValue); //发射信号
}
msleep(500); //线程休眠500ms
}
// 在 m_stop==true时结束线程任务
// quit();//相当于 exit(0),退出线程的事件循环
}
那么QDiceThread的需要定义公共函数,返回m_diceValue的值,如:
int QdiceThread::diceValue(){ return=m_diceValue;}
以便在主线程中调用此函数读取骰子的点数。
由于没有信号与槽的关联信号与槽的关系类似于硬件的中断与中断处理函数。主线程只能采用不断查询的方式主动查询是否有新并读取它,但是在主线程调用This value函数读取色子点数,使工作线程可能正在执行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():锁定互斥量,需要与lock()配对使用。
unlock():解锁一个互斥量,需要与lock()配对使用。
tryLock():试图锁定一个互斥量,如果成功锁定就返回true:如果其他线程已经锁定了这个互斥量,就犯规false,但不阻塞程序执行。
使用互斥量,对QDiceThread 类重新定义,不采用信号与槽机制,而是提供一个函数用于主线程读取数据。更改后QDiceTHread类定义如下:
#ifndef QDICETHREAD_H
#define QDICETHREAD_H
//#include <QObject>
#include <QThread>
#include <QMutex>
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()和unlock()进行了保护,这部分代码的执行就不会被其他线程中断。注意,lock()与unlock()必须配对使用。
在readValue()函数中,用互斥量 mutex的tryLock()和unlock()进行了保护。如果tryLock()成功锁定互斥量,读取数值的两行代码执行是不会被中断,执行完成解锁;如果tryLock()锁定失败,函数就立即返回,而不会等待。
原理上,对于两个或多个线程可能会同事读取活蟹的变量应该使用互斥量进行保护,例如QDiceThread中的变量m_stop 和m_paused,在run()函数中读取这两个变量,要再diceBegin(),diceEnd()和stopThread()函数里修改这些值,但这是3个函数都只有一个赋值语句,可以认为是原子操作,所以,可以不用锁定保护。
QMutex需要配对使用lock()和unlock()来实现代码段的保护,在一些逻辑复杂的代码段或可能发生异常的代码中,配对就可能出错。
QMutexLocker 是另外一个简化了互斥量处理的类。QMutexLocker 的构造函数接受一个互斥量作为参数并将其锁定,QMutexLocker的析构函数则将次互斥量解锁,所以在QmutexLocker 实例变量的生存期内代码段得到保护,自动进行互斥量的锁定和解锁。例如 run()函数可以如下更改:
void QDiceThread::run()
{
m_stop=false;//启动线程时令m_stop=false
m_seq=0;
qsrand(QTime::currentTime().msec());//随机数初始化,qsrand是线程安全的
while(!m_stop)//循环主体
{
if (!m_paused)
{
QMutexLocker Locker(&mutex);
m_diceValue=qrand(); //获取随机数
m_diceValue=(m_diceValue % 6)+1;
m_seq++;
mutex.unlock();
}
msleep(500); //线程休眠100ms
}
}
这样定义的QDiceThread类,在主程序中只能调用其readValue()函数来不断读取数值。上例采用QMutex进行线程同步,上例采用QMutex和QMutexLocker进行线程同步,其界面与第一个例子完全相同,值是增加了定时器,用于定时主动去读取制筛子线程数值。dialog类定义如下:
#ifndef DIALOG_H
#define DIALOG_H
#include <QDialog>
#include <QTimer>
#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
主要是增加了一个定时器m_Timer和其时间溢出响应槽函数 onTimeOut(),在Dialog的构造函数中将m_Timer的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);
}
}