此处需要说明的是,这里的线程同步概念与操作系统中的线程同步并无区别,都是避免多个线程同时访问临界区数据可能产生的读写错误问题。在 Qt 中,有多个类可以实现线程同步的功能,这些类包括 QMutex、QMutexLocker、 QReadWriteLock、QReadLocker、QWriteLocker、QWaitCondition、QSemaphore 等。
线程同步概念
在多线程程序中,由于存在多个线程,线程之间可能需要访问同一个变量,或一个线程需要等待另一个线程完成某个操作后才产生相应的动作。例如在上一节中提到的例程,工作线程生成随机的骰子点数,主线程读取骰子点数并显示,主线程需要等待工作线程生成一新的骰子点数后再读取数据。但上一节中并没有使用线程同步机制,而是使用了信号与槽的机制,在生成新的点数之后通过
信号通知主线程读取新的数据。这个过程类似于操作系统的线程信号机制,都是为信号设定一个处理函数,本章中不使用信号与槽函数来讲解其他方式的线程同步方法。
基于互斥量的线程同步
QMutex 和 QMutexLocker 是基于互斥量(mutex)的线程同步类。
QMutex类
该类提供的API函数如下:
void QMutex::lock() //锁定互斥量,一直等待
void QMutex::unlock() //解锁互斥量
bool QMutex::tryLock() //尝试锁定互斥量,不等待
bool QMutex::tryLock(int timeout) //尝试锁定互斥量,最多等待 timeout 毫秒
函数 lock()锁定互斥量,如果另一个线 程锁定了这个互斥量,它将被阻塞运行直到 其他线程解锁这个互斥量。函数 unlock()解锁互斥量,需要与 lock()配对使用。
函数 tryLock()尝试锁定一个互斥量,如果成功锁定就返回 true,如果其他线程已经锁定了这个互斥量就返回 false。函数 tryLock(int timeout)尝试锁定一个互斥量,如果这个互斥量被其他线程锁定,最多等待 timeout 毫秒。
互斥量相当于一把钥匙,如果两个线程要访问同一个共享资源,就需要通过 lock()或 tryLock()拿到这把钥匙,然后才可以访问该共享资源,访问完之后还要通过unlock()还回钥匙,这样别的线程才有机会拿到钥匙。
在上一节的例程中,在TDiceThread类中添加一个QMutex变量,并删除自定义信号newValue(),增加一个readValue()函数用于提供给主窗口访问类变量。
QMutex mutex; //互斥量
bool TDiceThread::readValue(int *seq, int *diceValue)
{
if (mutex.tryLock(100)) //尝试锁定互斥量,等待100ms
{
*seq=m_seq;
*diceValue=m_diceValue;
mutex.unlock(); //解锁互斥量
return true;
}
else
return false;
}
主函数(主线程)在访问m_seq和m_diceValue变量时,会尝试获取“钥匙”,最多等待100ms,得到权限后会通过指针类变量返回值。事后解锁以便于工作线程对这两个变量进行修改。
另一处需修改的是工作线程中的访问,代码如下:
void TDiceThread::run()
{//线程的事件循环
m_stop=false; //启动线程时令m_stop=false
m_paused=true; //启动运行后暂时不掷骰子
m_seq=0; //掷骰子次数
while(!m_stop) //循环主体
{
if (!m_paused)
{
mutex.lock(); //锁定互斥量
m_diceValue=0;
for(int i=0; i<5; i++)
m_diceValue += QRandomGenerator::global()->bounded(1,7); //产生随机数[1,6]
m_diceValue =m_diceValue/5;
m_seq++;
mutex.unlock(); //解锁互斥量
}
msleep(500); //线程休眠500ms
}
quit(); //在 m_stop==true时结束线程任务
}
在函数 run()中,我们对重新计算变量 m_diceValue 和 m_seq 值的代码片段用互斥量 mutex 进行了保护。工作线程运行后,其内部的函数 run()一直在运行。主线程里调用工作线程的 readValue()函数,其实际是在主线程里运行的。
通过上述方式,主线程和工作线程都对临界区的变量进行了互斥访问,这样就可确保数据的完整性。
QMutexLocker 类
QMutexLocker 是另一个简化了互斥量处理的类。QMutexLocker 的构造函数接受互斥量作为参数并将其锁定,QMutexLocker 的析构函数则将此互斥量解锁,所以在 QMutexLocker 实例变量的生存期内的代码片段会得到保护,自动进行互斥量的锁定和解锁。
void TDiceThread::run()
{//线程的事件循环
m_stop=false; //启动线程时令m_stop=false
m_paused=true; //启动运行后暂时不掷骰子
m_seq=0; //掷骰子次数
while(!m_stop) //循环主体
{
if (!m_paused)
{
QMutexLocker locker(&mutex);
m_diceValue=0;
for(int i=0; i<5; i++)
m_diceValue += QRandomGenerator::global()->bounded(1,7); //产生随机数[1,6]
m_diceValue =m_diceValue/5;
m_seq++;
}
msleep(500); //线程休眠500ms
}
quit(); //在 m_stop==true时结束线程任务
}
这两种实现互斥访问的功能一样,使用是分别注意其形式即可。
在主窗口类的构造函数中,设置了一个定时器,每隔一段时间读取一次临界区变量,如果成功获取到锁并且数据也是最新的,则据此更新主界面。
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
threadA= new TDiceThread(this);
connect(threadA,&TDiceThread::started, this, &MainWindow::do_threadA_started);
connect(threadA,&TDiceThread::finished,this, &MainWindow::do_threadA_finished);
timer= new QTimer(this); //创建定时器
timer->setInterval(200);
timer->stop();
connect(timer,&QTimer::timeout, this, &MainWindow::do_timeOut);
}
void MainWindow::do_timeOut()
{
int tmpSeq=0,tmpValue=0;
bool valid=threadA->readValue(&tmpSeq,&tmpValue); //读取数值
if (valid && (tmpSeq != m_seq)) //有效,并且是新数据
{
m_seq=tmpSeq;
m_diceValue=tmpValue;
QString str=QString::asprintf("第 %d 次掷骰子,点数为:%d",m_seq,m_diceValue);
ui->plainTextEdit->appendPlainText(str);
QString filename=QString::asprintf(":/dice/images/d%d.jpg",m_diceValue);
QPixmap pic(filename);
ui->labPic->setPixmap(pic);
}
}
在开始和结束按钮槽函数中,需要设置定时器的启动和停止。代码如下:
void MainWindow::on_actDice_Run_triggered()
{//"开始"按钮,开始掷骰子
threadA->diceBegin();
timer->start(); //重启定时器
ui->actDice_Run->setEnabled(false);
ui->actDice_Pause->setEnabled(true);
}
void MainWindow::on_actDice_Pause_triggered()
{//"暂停"按钮,暂停掷骰子
threadA->dicePause();
timer->stop(); //停止定时器
ui->actDice_Run->setEnabled(true);
ui->actDice_Pause->setEnabled(false);
}
基于读写锁的线程同步
使用互斥量时存在一个问题,即每次只能有一个线程获得互斥量的使用权限。如果在一个程序中有多个线程读取某个变量,使用互斥量时必须排队。而实际上若只是读取一个变量,可以让多个线程同时访问,这种情况下使用互斥量就会降低程序的性能。
因此提出了读写锁概念,Qt 提供了读写锁类 QReadWriteLock,它是基于读或写的方式进行代码片段锁定的,在多个线程读写一个共享数据时,使用它可以解决使用互斥量存在的上面所提到的问题。
QReadWriteLock 以 读或写锁定的同步方法允许以读或写的方式保护一段代码,它可以允许多个线程以只读方式同步访问资源,但是只要有一个线程在以写入方式访问资源,其他线程就必须等待,直到写操作结束。
简单总结就是:同一时间,多个线程可以同时读,只有一个线程可以写,读写不能同时进行。
QReadWriteLock类 提供以下几个主要的函数:
void lockForRead() //以只读方式锁定资源,如果有其他线程以写入方式锁定资源,这个函数会被阻塞
void lockForWrite() //以写入方式锁定资源,如果其他线程以读或写方式锁定资源,这个函数会被阻塞
void unlock() //解锁
bool tryLockForRead() //尝试以只读方式锁定资源,不等待
bool tryLockForRead(int timeout) //尝试以只读方式锁定资源,最多等待 timeout 毫秒
bool tryLockForWrite() //尝试以写入方式锁定资源,不等待
bool tryLockForWrite(int timeout) //尝试以写入方式锁定资源,最多等待 timeout 毫秒
例如下列案例:
int buffer[100];
QReadWriteLock Lock; //定义读写锁变量
void ThreadDAQ::run() //负责采集数据的线程
{ ...
Lock.lockForWrite(); //以写入方式锁定
get_data_and_write_in_buffer(); //数据写入 buffer
Lock.unlock();
...
}
void ThreadShow::run() //负责显示数据的线程
{ ...
Lock.lockForRead(); //以读取方式锁定
show_buffer(); //读取 buffer 里的数据并显示
Lock.unlock();
...
}
void ThreadSaveFile::run() //负责保存数据的线程
{ ...
Lock.lockForRead(); //以读取方式锁定
save_buffer_toFile(); //读取 buffer 里的数据并保存到文件
Lock.unlock();
...
}
另外,QReadLocker 和 QWriteLocker 是 QReadWriteLock 的简便形式,如同 QMutexLocker 是 QMutex 的简便形式一样,无须与 unlock()配对使用。