百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

从零开始学Qt(82):基于信号量的线程同步

liebian365 2024-10-17 14:00 38 浏览 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毫秒)。

在实际的数据采集中,要保证不丢失缓冲区或数据点,数据读取线程的速度必须快过数据写 入缓冲区的线程的速度。

相关推荐

深度解密epoll 如何工作的?(epoll基本处理流程)

epoll...

大乐透第19082期:头奖开出7注1000万分落六地 奖池41亿元

2019年7月17日晚开奖的体彩超级大乐透第19082期开奖号码为:前区06、18、20、21、31,后区03、04。本期大乐透前区号码五区比为1:0:3:0:1,二区和四区号码没有给出。当期前区和值...

【开奖】4月27日周六:福彩、体彩(2021年4月27日体彩开奖结果)

4月27日开奖福彩3D第2019110期:61222选5第2019110期:0812202122排列3第19110期:303排列5第19110期:30305大乐透第19047期:0304...

“红狒狒”落户哈尔滨铁路局(哈尔滨铁路红肠)

这几天,“红人”“红狒狒”在牡丹江机务段可引起了不小的轰动,众粉丝争相与其拍照留念,在该段人气爆棚!“红狒狒”到底何许人也?“红狒狒”,中文名:和谐3D型电力机车;绰号:红狒狒、番茄;制造商:大连机...

2D、3D、2.5D,做游戏还是搞噱头?玩家都晕了

前言游戏类型就像某种潮流,一种流行罢,另一种接棒成为主流。前两年的新作大多以“开放世界”为标签,在追求纯沙盒的过程中打造出一些细致的分类,比如说“类GTA沙盒”。诚然,纯碎的沙盒游戏并不多见,业内只有...

《战神4》PC版宣传片发布 GTX 1070即可60帧畅玩

在今年10月的时候索尼PlayStation官方正式宣布圣莫尼卡2018年的《战神4》将于2022年1月14日推出PC版本,官方在今天公布了一段PC版宣传片,并且公开了游戏的配置需求。下面让我们一起来...

男星深情好丈夫形象崩塌,半夜搂美女坐大腿,举止亲密

近日,于晓光被拍到深夜在酒吧玩,结束后与一名女子一起上车离开。上车后,女子直接坐在了他腿上,他也顺势搂着美女,美女满脸笑容地坐在他腿上玩手机离开。可能有人会好奇,于晓光是谁呢?于晓光是韩国艺人秋瓷炫的...

d3d12dll丢失怎么修复?d3d12dll加载失败怎么解决?

  d3d12.dll丢失怎么修复?d3d12.dll加载失败怎么解决?很多朋友想要运行游戏的时候都会遇到这个问题,这种情况该怎么办呢?今天系统之家小编给朋友们讲讲具体的解决方法,操作其实还蛮简单的。...

许多玩家反馈《生化4RE》PC一直崩溃 无法进入游戏

今日(3月24日),卡普空《生化危机4:重制版》正式发售,然而有部分PC玩家遇到了游戏崩溃等问题。很多玩家在贴吧发帖称游戏遇到了严重的崩溃问题,且经常反复,报错代码普遍为FatalD3Derror...

微软正式推出适用于WSL Linux的D3D12 GPU视频加速技术

今天,微软正式向WindowsSubsystemforLinux(WSL)用户发布了Direct3D12GPU视频加速支持。在微软通过WSL允许在Linux下使用Open...

《怪物猎人:崛起》曙光系统报错“Fatal d3d error”的解决办法

《怪物猎人:崛起》曙光系统报错“Fatald3derror”的解决办法不少小伙伴反应《怪物猎人:崛起》DLC曙光预载以后打不开游戏,出现了Fatald3derror类似的错误代码,这类问题的解...

Mac+双屏,前端程序员的专业配置 - Loctek 乐歌 D3D 双屏电脑显示器支架

做FE也有一段日子了,电脑屏幕每天在设计稿、浏览器、IDE、即时通讯工具、Terminal、邮箱之间切换。虽然mac的工作区带来了很多灵活,但是依然略显不足。于是入手支架,把公司配的电脑和显示器发挥起...

RPC 的原理和简单使用(rpc详解)

RPC的概念RPC,RemoteProcedureCall,翻译成中文就是远程过程调用,是一种进程间通信方式。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数。在调用的...

大厂开源的golang微服务rpc框架 — kitex

提前rpc估计所有的开发同学都知道,不知道的也无所谓,毕竟我也好几年没用了,今天带大家在复习一下。RPC(RemoteProcedureCall):远程过程调用,...

干货!一文掌握Protobuf所有语言所有用法,快收藏

说实话,Protobuf这个库,让人相见时难别亦难,东风无力百花残,每次等到要用它的时候,总感觉还没有完全掌握它的用法,而实际上等去百度或者谷歌的时候,教程都是多么的凌乱不堪。学会它,最直接关系到的,...

取消回复欢迎 发表评论: