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++开发指南

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/938460.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

js 数组方法总结

在 JavaScript 中&#xff0c;数组有许多内置的方法&#xff0c;可以用于操作和处理数组。以下是一些常用的数组方法及其特点&#xff1a; 1. push() - 用途&#xff1a;向数组末尾添加一个或多个元素 - 改变原数组&#xff1a;是 - 返回值&#xff1a;返回数组的新长度 let ar…

MongoDB-副本集

一、什么是 MongoDB 副本集&#xff1f; 1.副本集的定义 MongoDB 的副本集&#xff08;Replica Set&#xff09;是一组 MongoDB 服务器实例&#xff0c;它们存储同一数据集的副本&#xff0c;确保数据的高可用性和可靠性。副本集中的每个节点都有相同的数据副本&#xff0c;但…

驱动开发-入门【1】

1.内核下载地址 Linux内核源码的官方网站为https://www.kernel.org/&#xff0c;可以在该网站下载最新的Linux内核源码。进入该网站之后如下图所示&#xff1a; 从上图可以看到多个版本的内核分支&#xff0c;分别为主线版本&#xff08;mainline&#xff09;、稳定版本&#…

数字电视标准与分类

数字电视相关内容是一个极其成熟且久远的领域&#xff0c;并不像其它的技术方面那么前沿。但是学习技术的另外一个方面也不就是可以维持咱们的好奇心以及认识生活中多个事务后面的技术本质。 近年来&#xff0c;电视领域发生了一系列的变化&#xff0c;电视数字化的进程明显加快…

【WRF安装】WRF编译错误总结1:HDF5库包安装

目录 1 HDF5库包安装有误&#xff1a;HDF5 not set in environment. Will configure WRF for use without.HDF5的重新编译 错误原因1&#xff1a;提示 overflow 错误1. 检查系统是否缺少依赖库或工具2. 检查和更新编译器版本3. 检查 ./configure 报错信息4. 检查系统环境变量5.…

51c嵌入式~单片机~合集3

我自己的原文哦~ https://blog.51cto.com/whaosoft/12362395 一、STM32代码远程升级之IAP编程 IAP是什么 有时项目上需要远程升级单片机程序&#xff0c;此时需要接触到IAP编程。 IAP即为In Application Programming&#xff0c;解释为在应用中编程&#xff0c;用户自己的…

LeetCode 11. 盛最多水的容器(超简单讲解)

11. 盛最多水的容器 题目示例示例1示例2 解题思路双指针实现设计 详细代码 题目 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多…

Spring Boot 集成 Elasticsearch怎样在不启动es的情况下正常启动服务

解释 在spingboot 集成es客户端后&#xff0c;每当服务启动时&#xff0c;服务默认都会查看es中是否已经创建了对应的索引&#xff0c;如果没有索引则创建。基于上面的规则我们可以通过配置不自动创建索引来达到在没有es服务的情况下正常启动服务。 解决办法 在entity类的Docu…

IOTIQS100芯片, TCP 发送数据+NSOSD,data要是hex16进制转换方法

命令&#xff1a;data以十六进制字符串格式发送的数据。 方法 代码 sprintf(temp, "%02X", data[i]);&#xff1a;将当前字节转换为两位宽的大写十六进制字符&#xff0c;并存储在 temp 中。如果需要小写字母&#xff0c;可以将格式说明符改为 "%02x"。 …

Python的3D可视化库【vedo】2-3 (plotter模块) 增删物体、控制相机

文章目录 4 Plotter类的方法4.3 渲染器内的物体操作4.3.1 添加物体4.3.2 移除物体4.3.3 渲染器的内容列表 4.4 相机控制4.4.1 访问相机对象4.4.2 重置相机状态4.4.3 移动相机位置4.4.4 改变相机焦点4.4.5 改变相机朝向的平面4.4.5 旋转相机4.4.6 对齐相机的上朝向4.4.7 缩放 ve…

Mumu模拟器12开启ADB调试方法

在使用安卓模拟器进行开发或调试时&#xff0c;ADB&#xff08;Android Debug Bridge&#xff09;是一项不可或缺的工具。大多数模拟器默认开启了ADB调试功能&#xff0c;但在安装最新版的 Mumu模拟器12 时&#xff0c;可能会遇到 adb devices 无法识别设备的问题。 问题描述 …

【OpenCV计算机视觉】图像处理——平滑

本篇文章记录我学习【OpenCV】图像处理中关于“平滑”的知识点&#xff0c;希望我的分享对你有所帮助。 目录 一、什么是平滑处理 1、平滑的目的是什么&#xff1f; 2、常见的图像噪声 &#xff08;1&#xff09;椒盐噪声 ​编辑&#xff08;2&#xff09; 高斯噪声 &a…

vue CSS 自定义宽高 翻页 剥离 效果

新增需求&#xff0c;客户需要类似PPT的剥离效果用于WEB页面翻页&#xff0c;查找资料后&#xff0c;参考下方的掘金博主的文章&#xff0c;并将HTML修改成vue的页面进行使用。其中宽度、高度改成了变量&#xff0c;样式style中的属性与宽高的关系整理成了公式进行动态计算。 …

单北斗+鸿蒙系统+国产芯片,遨游防爆手机自主可控“三保险”

在当今全球科技竞争日益激烈的背景下&#xff0c;技术自主可控的重要性愈发凸显。它不仅关乎国家安全&#xff0c;更是推动产业升级和经济发展的关键。特别是在一些特殊领域&#xff0c;如防爆通信&#xff0c;自主可控的技术更是不可或缺。遨游通讯推出了一款融合了单北斗、鸿…

Word2Vec:将词汇转化为向量的技术

文章目录 Word2Vec来龙去脉分层Softmax负采样 Word2Vec 下面的文章纯属笔记&#xff0c;看完后不会有任何收获&#xff0c;如果想理解这两种优化技术&#xff0c;给大家推荐一篇博客&#xff0c;讲的很好&#xff1a; 详解-----分层Softmax与负采样 来龙去脉 word2vec,即将词…

虚幻5描边轮廓材质

很多游戏内都有这种描边效果&#xff0c;挺实用也挺好看的&#xff0c;简单复刻一下 效果演示&#xff1a; Linethickness可以控制轮廓线条的粗细 这样连完&#xff0c;然后放到网格体细节的覆层材质上即可 可以自己更改粗细大小和颜色

websocket_asyncio

WebSocket 和 asyncio 指南 简介 本指南涵盖了使用 Python 中的 websockets 库进行 WebSocket 编程的基础知识&#xff0c;以及 asyncio 在异步非阻塞 I/O 中的作用。它提供了构建高效 WebSocket 服务端和客户端的知识&#xff0c;以及 asyncio 的特性和优势。 1. 什么是 WebS…

序列模型的使用示例

序列模型的使用示例 1 RNN原理1.1 序列模型的输入输出1.2 循环神经网络&#xff08;RNN&#xff09;1.3 RNN的公式表示2 数据的尺寸 3 PyTorch中查看RNN的参数4 PyTorch中实现RNN&#xff08;1&#xff09;RNN实例化&#xff08;2&#xff09;forward函数&#xff08;3&#xf…

Hadoop学习笔记(包括hadoop3.4.0集群安装)(黑马)

Hadoop学习笔记 0-前置章节-环境准备 0.1 环境介绍 配置环境&#xff1a;hadoop-3.4.0&#xff0c;jdk-8u171-linux-x64 0.2 VMware准备Linux虚拟机 0.2.1主机名、IP、SSH免密登录 1.配置固定IP地址&#xff08;root权限&#xff09; 开启master&#xff0c;修改主机名为…

【计算机网络】Layer4-Transport layer

目录 传输层协议How demultiplexing works in transport layer&#xff08;传输层如何进行分用&#xff09;分用&#xff08;Demultiplexing&#xff09;的定义&#xff1a;TCP/UDP段格式&#xff1a; UDPUDP的特点&#xff1a;UDP Format端口号Trivial File Transfer Protocol…