在多线程的程序中,多个线程之间的同步问题实际上就是多个线程之间的协调问题。例如在以下例子中只有等 ThreadDAQ 写满一个缓冲区之后,ThreadShow 和ThreadSaveFile 才能读取缓冲区的数据。
int buffer[100];
QReadWriteLock Lock; //定义读写锁变量
void ThreadDAQ::run() //负责采集数据的线程
{ ...
QWriteLocker Locker(&Lock); //以写入方式锁定
get_data_and_write_in_buffer(); //数据写入 buffer
...
}
void ThreadShow::run() //负责显示数据的线程
{ ...
QReadLocker Locker(&Lock); //以读取方式锁定
show_buffer(); //读取 buffer 里的数据并显示
...
}
void ThreadSaveFile::run() //负责保存数据的线程
{ ...
QReadLocker Locker(&Lock); //以读取方式锁定
save_buffer_toFile(); //读取 buffer 里的数据并保存到文件
...
}
采用互斥量和读写锁的方法都是对资源的锁定和解锁,避免同时访问资源时产生冲突。但是一个线程解锁资源后,不能及时通知其他线程。
QWaitCondition 提供了一种改进的线程同步方法,QWaitCondition 通过与 QMutex 或QReadWriteLock 结合使用,可以使一个线程在满足一定条件时通知其他多个线程,使其他多个线程及时进行响应,这样比只使用互斥量或读写锁效率要高一些。
QWaitCondition 提供如下一些函数:
bool wait(QMutex *lockedMutex, unsigned long time) //释放互斥量,并等待唤醒
bool wait(QReadWriteLock *lockedReadWriteLock, unsigned long time)
//释放读写锁,并等待唤醒
void wakeAll() //唤醒所有处于等待状态的线程,唤醒线程的顺序不确定,由操作系统的调度策略决定
void wakeOne() //唤醒一个处于等待状态的线程,唤醒哪个线程不确定,由操作系统的调度策略决定
QWaitCondition 一般用于生产者/消费者(producer/consumer)模型。生产者产生数据,消费者使用数据,前面讲述的数据采集、显示与存储的三线程例子就适用于这种模型。
示例程序解读
示例程序实现的功能与前几节无异,只是改成使用QReadWriteLock 和 QWaitCondition 类将掷骰子程序按生产者/消费者模型进行修改。
工作线程
对于工作线程的头文件和定义如下:
///.h文件
#ifndef TDICETHREAD_H
#define TDICETHREAD_H
#include <QThread>
//TDiceThread 是产生骰子点数的线程
class TDiceThread : public QThread
{
Q_OBJECT
protected:
void run(); //线程的任务函数
public:
explicit TDiceThread(QObject *parent = nullptr);
};
//TValueThread 获取骰子点数
class TValueThread : public QThread
{
Q_OBJECT
protected:
void run(); //线程的任务函数
public:
explicit TValueThread(QObject *parent = nullptr);
signals:
void newValue(int seq, int diceValue);
};
//TPictureThread获取骰子点数,生成对应的图片文件名
class TPictureThread : public QThread
{
Q_OBJECT
protected:
void run(); //线程的任务函数
public:
explicit TPictureThread(QObject *parent = nullptr);
signals:
void newPicture(QString picName);
};
#endif // TDICETHREAD_H
.cpp文件///
#include "tdicethread.h"
#include <QRandomGenerator>
#include <QReadWriteLock>
#include <QWaitCondition>
QReadWriteLock rwLocker;
QWaitCondition waiter;
int seq=0, diceValue=0;
TDiceThread::TDiceThread(QObject *parent)
: QThread{parent}
{
}
void TDiceThread::run()
{//线程的任务函数
seq=0;
while(1)
{
rwLocker.lockForWrite(); //以写方式锁定
diceValue = QRandomGenerator::global()->bounded(1,7); //产生随机数[1,6]
seq++;
rwLocker.unlock(); //解锁
waiter.wakeAll(); //唤醒其他等待的线程
msleep(500); //线程休眠500ms
}
}
void TValueThread::run()
{
while(1)
{
rwLocker.lockForRead(); //以只读方式锁定
waiter.wait(&rwLocker); //等待被唤醒
emit newValue(seq,diceValue);
rwLocker.unlock(); //解锁
}
}
TValueThread::TValueThread(QObject *parent)
:QThread{parent}
{
}
void TPictureThread::run()
{
while(1)
{
rwLocker.lockForRead(); //以只读方式锁定
waiter.wait(&rwLocker); //等待被唤醒
QString filename=QString::asprintf(":/dice/images/d%d.jpg",diceValue);
emit newPicture(filename);
rwLocker.unlock(); //解锁
}
}
TPictureThread::TPictureThread(QObject *parent)
:QThread{parent}
{
}
TDiceThread 线程负责生成骰子点数;TValueThread 线程读取最新的骰子点数,并用信号 newValue()将其发射出去;TPictureThread 线程读取最新的骰子点数,转换为图片文件名,并用信号 newPicture()将其发射出去。
在生产者/消费者模型中,TDiceThread 是生产者,TValueThread 和TPictureThread 是消费者。
TDiceThread::run()函数每隔 500 毫秒生成一次数据,新数据生成后唤醒所有等待的线程:
waiter.wakeAll(); //唤醒所有等待的线程
TValueThread::run()函数中,在 while 循环体内,线程先用 rwLocker.lockForRead()以只读方式锁定读写锁,再运行下面的一条语句:
waiter.wait(&rwLocker); //等待被唤醒
这条语句以 rwLocker 作为输入参数,内部会首先释放 rwLocker,使其他线程可以锁定rwLocker,
TValueThread 线程进入等待状态。当 TDiceThread 线程生成新数据,并使用 waiter.wakeAll()唤醒所有等待的线程后,TValueThread 线程会再次锁定 rwLocker,然后退出阻塞状态,运行后面的代码。TValueThread 线程发射信号 newValue()后,再运行 rwLocker.unlock()正式解锁rwLocker。
主窗口设计
在MainWindow的构造函数中,创建这三个线程,并连接其对应的槽函数:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
threadA= new TDiceThread(this); //producer
threadValue= new TValueThread(this); //consumer 1
threadPic= new TPictureThread(this); //consumer 2
connect(threadA,&TDiceThread::started, this, &MainWindow::do_threadA_started);
connect(threadA,&TDiceThread::finished,this, &MainWindow::do_threadA_finished);
connect(threadValue,&TValueThread::newValue,this, &MainWindow::do_newValue);
connect(threadPic,&TPictureThread::newPicture,this, &MainWindow::do_newPicture);
}
其中,do_newValue和do_newPicture是两个工作线程用于提醒主界面更新的信号,代码如下:
void MainWindow::do_newValue(int seq, int diceValue)
{
QString str=QString::asprintf("第 %d 次掷骰子,点数为:%d",seq,diceValue);
ui->plainTextEdit->appendPlainText(str);
}
void MainWindow::do_newPicture(QString picName)
{
QPixmap pic(picName);
ui->labPic->setPixmap(pic);
}
在主窗口中点击启动与结束对应的槽函数如下:
void MainWindow::on_actThread_Run_triggered()
{//"启动线程"按钮
threadValue->start();
if (! threadPic->isRunning())
threadPic->start();
if(! threadA->isRunning())
threadA->start();
}
void MainWindow::on_actThread_Quit_triggered()
{//"结束线程"按钮
threadA->terminate();
threadA->wait();
}
几个线程启动的先后顺序不能调换,应先启动 threadValue 和 threadPic,使它们先进入等待状态,最后启动 threadA。这样在 threadA 里调用 wakeAll()时 threadValue 和 threadPic 就可以及时响应,否则会丢失第一次掷骰子的数据。
点击结束时,只是终止了线程 threadA,而没有终止线程 threadValue和 threadPic,但是因为它们不会被唤醒了,所以不会再发射信号。所以,点击“结束线程”按钮后,界面上不会出现新的数据和图片。
主界面点击x号关闭界面时,保证所有线程在主界面关闭后都退出:
void MainWindow::closeEvent(QCloseEvent *event)
{
if (threadA->isRunning())
{
threadA->terminate(); //强制结束线程
threadA->wait(); //等待线程结束
}
if (threadValue->isRunning())
{
threadValue->terminate(); //强制结束线程
threadValue->wait(); //等待线程结束
}
if (threadPic->isRunning())
{
threadPic->terminate(); //强制结束线程
threadPic->wait(); //等待线程结束
}
event->accept();
}
互斥量、读写锁等都是线程间通信 (inter-thread communication,ITC)底层技术,Qt 的信号与槽是一种对象间通信(inter-object communication)技术,这些对象可以在同一个线程内,也可以在不同的线程内,所以信号与槽也可以用于 ITC。在设计多线程应用程序时,建议尽量使用信号与槽进行通信,无法用信号与槽解决时再用专门的 ITC 技术。
参考
Qt6 C++开发指南