QT多线程(三):基于条件等待的线程同步

在多线程的程序中,多个线程之间的同步问题实际上就是多个线程之间的协调问题。例如在以下例子中只有等 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++开发指南

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值