从零开始学Qt(82):基于信号量的线程同步
liebian365 2024-10-17 14:00 27 浏览 0 评论
信号量的原理
信号量(Semaphore)是另一种限制对共享资源进行访问的线程同步机制,它与互斥量(Mutex) 相似,但是有区别。一个互斥量只能被锁定一次,而信号量可以多次使用。信号量通常用来保护一定数量的相同的资源,如数据采集时的双缓冲区。
QSemaphore是实现信号量功能的类,它提供以下几个基本的函数:
- acquire(int n),尝试获得n个资源。如果没有这么多资源,线程将阻塞直到有n个资源可用;
- release(int n),释放n个资源,如果信号量的资源己全部可用之后再release()就可以创建更多的资源,增加可用资源的个数;
- int available(),返回当前信号量可用的资源个数,这个数永远不可能为负数,如果为0,就说明当前没有资源可用;
- bool tryAcquire(int n=1),尝试获取n个资源,不成功时不阻塞线程。
定义QSemaphore的实例时,可以传递一个数值作为初始可用的资源个数。
下面的一段示意代码,说明QSemaphore的几个函数的作用。
QSemaphore WC(5); // WC.available()==5,初始资源个数为5个
WC.acquire(4); // WC.available()==1,用了 4个资源,还剩余1个可用
WC.release(2); // WC.available()==3,释放了 2个资源,剩余3个可用
WC.acquire(3); // WC.available()==0, 又用了 3个资源,剩余0个可用
WC.tryAcquire(1); //因为 WC.available()==0,返回false,
WC.acquire(); // 因为WC.available()==0,没有资源可用,阻塞
互斥量相当于列车上的卫生间,一次只允许一个人进出,信号量则是多人公共卫生间,允许多人进出。n个资源就是信号量需要保护的共享资源,至于资源如何分配,就是内部处理的问题了。
双缓冲区数据采集和读取线程类设计
信号量通常用来保护一定数量的相同资源,如数据采集时的双缓冲区,适用于 Producer/Consumer 模型。
在实例中,创建类似于Producer/Consumer模型的两个线程类QThreadDAQ和 QThreadShow。mythread.h文件中这两个类的定义如下:
class QThreadDAQ : public QThread
{
Q_OBJECT
private:
bool m_stop=false; //停止线程
protected:
void run() Q_DECL_OVERRIDE;
public:
QThreadDAQ();
void stopThread();
};
class QThreadShow : public QThread
{
Q_OBJECT
private:
bool m_stop=false; //停止线程
protected:
void run() Q_DECL_OVERRIDE;
public:
QThreadShow();
void stopThread();
signals:
void newValue(int *data,int count,int seq);
};
QThreadDAQ是数据采集线程,例如在使用数据采集卡进行连续数据采集时,需要一个单独 的线程将采集卡采集的数据读取到缓冲区内。
QThreadShow是数据读取线程,用于读取己存满数据的缓冲区中的数据并传递给主线程显示,采用信号与槽机制与主线程交互。
QThreadDAQ/QThreadShow类的定义与使用QWaitCondition实例中的 QThreadProducer/QThreadConsumer类的定义类似,只是QThreadShow的信号newValue()采用了指针作为传递参数,用于一次传递出一个缓冲区的数据。
mythread.cpp文件中QThreadDAQ和QThreadShow的主要功能代码如下:
void QThreadDAQ::run()
{
m_stop=false; //启动线程时令 m_stop=false
bufNo=0; //缓冲区序号
curBuf=1; //当前写入使用的缓冲区
counter=0; //数据生成器
int n=emptyBufs.available();
if(n<2) //保证线程启动时 emptyBufs.available==2
emptyBufs.release(2-n);
while(!m_stop){ //循环主体
emptyBufs.acquire(); //获取一个空的缓冲区
for(int i=0;i<BufferSize;i++){ //产生一个缓冲区的数据
if(curBuf==1)
buffer1[i]=counter; //向缓冲区写入数据
else
buffer2[i]=counter;
counter++; //模拟数据采集卡产生数据
msleep(50); //每 50ms 产生一个数
}
bufNo++; //缓冲区序号
if(curBuf==1) //切换当前写入缓冲区
curBuf=2;
else
curBuf=1;
fullBufs.release();//有了一个满的缓冲区,available==1
}
}
void QThreadShow::run()
{
m_stop=false;
int n=fullBufs.available();
if(n>0)
fullBufs.acquire(n); //将fullBufs可用资源个数初始化为0
while(!m_stop){ //循环主体
fullBufs.acquire(); //等待有缓冲区满,当 fullBufs.available==0 阻塞
int bufferData[BufferSize];
int seq=bufNo;
if(curBuf==1) //当前在写入的缓冲区是1,那么满的缓冲区是2
for(int i=0;i<BufferSize;i++)
bufferData[i]=buffer2[i] ; //快速拷贝缓冲区数据
else
for(int i=0;i<BufferSize;i++)
bufferData[i]=buffer1[i];
emptyBufs.release();//释放一个空缓冲区
emit newValue(bufferData, BufferSize, seq) ; //给主线程传递数据
}
}
在共享变量区定义了两个缓冲区buffer1和buffer2,都是长度为BufferSize的数组。
变量curBuf记录当前写入操作的缓冲区编号,其值只能是1或2,表示buffer1或buffer2, bufNo是累积的缓冲区个数编号,counter是模拟采集数据的变量。
信号量emptyBufs初始资源个数为2,表示有2个空的缓冲区可用。
信号量fullBufs初始化资源个数为0,表示写满数据的缓冲区个数为零。
QThreadDAQ::run()采用双缓冲方式进行模拟数据采集,线程启动时初始化共享变量,特别的是使emptyBufs的可用资源个数初始化为2。
在while循环体里,第一行语句emptyBufs.acquire()使信号量emptyBufs获取一个资源,即获取一个空的缓冲区。用于数据缓存的有两个缓冲区,只要有一个空的缓冲区,就可以向这个缓冲区写入数据。
while循环体里的for循环每隔50毫秒使counter值加1,然后写入当前正在写入的缓冲区, 当前写入哪个缓冲区由curBuf决定。counter是模拟采集的数据,连续增加可以判断采集的数据是否连续。
完成for循环后正好写满一个缓冲区,这时改变curBuf的值,切换用于写入的缓冲区。
写满一个缓冲区之后,使用fullBufs.release()为信号量fullBufs释放一个资源,这时fullBufs. available=1,表示有一个缓冲区被写满了。这样,QThreadShow线程里使用fullBufs.acquire()就可以获得一个资源,可以读取己写满的缓冲区里的数据。
QThreadShow::run()用于监测是否有己经写满数据的缓冲区,只要有缓冲区写满了数据,就立刻读取出数据,然后释放这个缓冲区给QThreadDAQ线程用于写入。
QThreadShow::run()函数的初始化部分使fullBufs. available==0,即线程刚启动时是没有资源的。
在while循环体里第一行语句就是通过fullBufs.acquire()以阻塞方式获取一个资源,只有当 QThreadDAQ线程里写满一个缓冲区,执行一次fullBufs.release()后,fullBufs.acquire()才获得资源并执行后面的代码。后面的代码就立即用临时变量将缓冲区里的数据读取出来,再调用emptyBufs.release()给信号量emptyBufs释放一个资源,然后发射信号newValue,由主线程读取数据并显示。
所以,这里使用了双缓冲区、两个信号量实现采集和读取两个线程的协调操作。采集线程里 使用emptyBufs.acquire()获取可以写入的缓冲区。
实际使用数据采集卡进行连续数据采集时,采集线程是不能停顿下来的,也就是说万一读取 线程执行较慢,采集线程是不会等待的。所以实际情况下,读取线程的操作应该比采集线程快。
QThreadDAQ 和 QThreadShow 的使用
设计窗口基于QWidget应用程序,类定义如下(省略了一些不重要的或与前面实例重复的部分内容):
class Widget : public QWidget
{
Q_OBJECT
private:
QThreadDAQ threadProcuder;
QThreadShow threadConsumer;
private slots:
void onthreadB_newValue(int *data, int count, int bufNo);
};
Widget 类定义了两个线程的实例,threadProducer 和 threadConsumer。
自定义了一个槽函数onthreadB_newValue(),用于与threadConsumer的信号关联,在Widget的构造函数里进行了关联。
connect(&threadConsumer,SIGNAL(newValue(int*,int,int)),this,SLOT(onthreadB_newValue(int*,int,int)));
槽函数onthreadB_newValue()的功能就是读取一个缓冲区里的数据并显示,其实现代码如下:
void Widget::onthreadB_newValue(int *data, int count, int bufNo)
{//读取threadConsumer传递的缓冲区的数据
QString str=QString::asprintf("第 %d 个缓冲区:",bufNo);
for (int i=0;i<count;i++){
str=str+QString::asprintf("%d, ",*data);
data++;
}
str=str+'\n';
ui->plainTextEdit->appendPlainText(str);
}
传递的指针型参数int *data是一个数组指针,count是缓冲区长度。
“启动线程”和“结束线程”两个按钮的代码如下(省略了按键使能控制的代码):
void Widget::on_btnStart_clicked()
{//启动线程
threadConsumer.start();
threadProducer.start();
}
void Widget::on_btnStop_clicked()
{//结束线程
threadConsumer.terminate();
threadConsumer.wait();
threadProducer.terminate();
threadProducer.wait();
}
启动线程时,先启动threadConsumer,再启动threadProducer,否则可能丢失第1个缓冲区的数据。
结束线程时,都采用terminate()函数强制结束线程,因为两个线程之间有互锁的关系,若不使用terminate()强制结束会出现线程无法结束的问题。 程序运行时的界面如图所示。
从图可以看出,没有出现丢失缓冲区或数据点的情况,两个线程之间协调的很好,将 QThreadDAQ::run()函数中模拟采样率的延时时间调整为2毫秒也没问题(正常设置为50毫秒)。
在实际的数据采集中,要保证不丢失缓冲区或数据点,数据读取线程的速度必须快过数据写 入缓冲区的线程的速度。
相关推荐
- 4万多吨豪华游轮遇险 竟是因为这个原因……
-
(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...
- “菜鸟黑客”必用兵器之“渗透测试篇二”
-
"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...
- 科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白
-
作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...
- 麦子陪你做作业(二):KEGG通路数据库的正确打开姿势
-
作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...
- 知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势
-
智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...
- 每日新闻播报(September 14)_每日新闻播报英文
-
AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...
- 香港新巴城巴开放实时到站数据 供科技界研发使用
-
中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...
- 5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper
-
本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...
- Qt动画效果展示_qt显示图片
-
今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...
- 如何从0到1设计实现一门自己的脚本语言
-
作者:dong...
- 三年级语文上册 仿写句子 需要的直接下载打印吧
-
描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...
- C++|那些一看就很简洁、优雅、经典的小代码段
-
目录0等概率随机洗牌:1大小写转换2字符串复制...
- 二年级上册语文必考句子仿写,家长打印,孩子照着练
-
二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)