爆款云主机2核4G限时秒杀,88元/年起!
查看详情

活动

天翼云最新优惠活动,涵盖免费试用,产品折扣等,助您降本增效!
热门活动
  • 618智算钜惠季 爆款云主机2核4G限时秒杀,88元/年起!
  • 免费体验DeepSeek,上天翼云息壤 NEW 新老用户均可免费体验2500万Tokens,限时两周
  • 云上钜惠 HOT 爆款云主机全场特惠,更有万元锦鲤券等你来领!
  • 算力套餐 HOT 让算力触手可及
  • 天翼云脑AOne NEW 连接、保护、办公,All-in-One!
  • 中小企业应用上云专场 产品组合下单即享折上9折起,助力企业快速上云
  • 息壤高校钜惠活动 NEW 天翼云息壤杯高校AI大赛,数款产品享受线上订购超值特惠
  • 天翼云电脑专场 HOT 移动办公新选择,爆款4核8G畅享1年3.5折起,快来抢购!
  • 天翼云奖励推广计划 加入成为云推官,推荐新用户注册下单得现金奖励
免费活动
  • 免费试用中心 HOT 多款云产品免费试用,快来开启云上之旅
  • 天翼云用户体验官 NEW 您的洞察,重塑科技边界

智算服务

打造统一的产品能力,实现算网调度、训练推理、技术架构、资源管理一体化智算服务
智算云(DeepSeek专区)
科研助手
  • 算力商城
  • 应用商城
  • 开发机
  • 并行计算
算力互联调度平台
  • 应用市场
  • 算力市场
  • 算力调度推荐
一站式智算服务平台
  • 模型广场
  • 体验中心
  • 服务接入
智算一体机
  • 智算一体机
大模型
  • DeepSeek-R1-昇腾版(671B)
  • DeepSeek-R1-英伟达版(671B)
  • DeepSeek-V3-昇腾版(671B)
  • DeepSeek-R1-Distill-Llama-70B
  • DeepSeek-R1-Distill-Qwen-32B
  • Qwen2-72B-Instruct
  • StableDiffusion-V2.1
  • TeleChat-12B

应用商城

天翼云精选行业优秀合作伙伴及千余款商品,提供一站式云上应用服务
进入甄选商城进入云市场创新解决方案
办公协同
  • WPS云文档
  • 安全邮箱
  • EMM手机管家
  • 智能商业平台
财务管理
  • 工资条
  • 税务风控云
企业应用
  • 翼信息化运维服务
  • 翼视频云归档解决方案
工业能源
  • 智慧工厂_生产流程管理解决方案
  • 智慧工地
建站工具
  • SSL证书
  • 新域名服务
网络工具
  • 翼云加速
灾备迁移
  • 云管家2.0
  • 翼备份
资源管理
  • 全栈混合云敏捷版(软件)
  • 全栈混合云敏捷版(一体机)
行业应用
  • 翼电子教室
  • 翼智慧显示一体化解决方案

合作伙伴

天翼云携手合作伙伴,共创云上生态,合作共赢
天翼云生态合作中心
  • 天翼云生态合作中心
天翼云渠道合作伙伴
  • 天翼云代理渠道合作伙伴
天翼云服务合作伙伴
  • 天翼云集成商交付能力认证
天翼云应用合作伙伴
  • 天翼云云市场合作伙伴
  • 天翼云甄选商城合作伙伴
天翼云技术合作伙伴
  • 天翼云OpenAPI中心
  • 天翼云EasyCoding平台
天翼云培训认证
  • 天翼云学堂
  • 天翼云市场商学院
天翼云合作计划
  • 云汇计划
天翼云东升计划
  • 适配中心
  • 东升计划
  • 适配互认证

开发者

开发者相关功能入口汇聚
技术社区
  • 专栏文章
  • 互动问答
  • 技术视频
资源与工具
  • OpenAPI中心
开放能力
  • EasyCoding敏捷开发平台
培训与认证
  • 天翼云学堂
  • 天翼云认证
魔乐社区
  • 魔乐社区

支持与服务

为您提供全方位支持与服务,全流程技术保障,助您轻松上云,安全无忧
文档与工具
  • 文档中心
  • 新手上云
  • 自助服务
  • OpenAPI中心
定价
  • 价格计算器
  • 定价策略
基础服务
  • 售前咨询
  • 在线支持
  • 在线支持
  • 工单服务
  • 建议与反馈
  • 用户体验官
  • 服务保障
  • 客户公告
  • 会员中心
增值服务
  • 红心服务
  • 首保服务
  • 客户支持计划
  • 专家技术服务
  • 备案管家

了解天翼云

天翼云秉承央企使命,致力于成为数字经济主力军,投身科技强国伟大事业,为用户提供安全、普惠云服务
品牌介绍
  • 关于天翼云
  • 智算云
  • 天翼云4.0
  • 新闻资讯
  • 天翼云APP
基础设施
  • 全球基础设施
  • 信任中心
最佳实践
  • 精选案例
  • 超级探访
  • 云杂志
  • 分析师和白皮书
  • 天翼云·创新直播间
市场活动
  • 2025智能云生态大会
  • 2024智算云生态大会
  • 2023云生态大会
  • 2022云生态大会
  • 天翼云中国行
天翼云
  • 活动
  • 智算服务
  • 产品
  • 解决方案
  • 应用商城
  • 合作伙伴
  • 开发者
  • 支持与服务
  • 了解天翼云
      • 文档
      • 控制中心
      • 备案
      • 管理中心

      QtThread线程同步和缓冲区设计

      首页 知识中心 软件开发 文章详情页

      QtThread线程同步和缓冲区设计

      2024-12-05 08:50:14 阅读次数:20

      互斥,代码,函数,数据,线程,缓冲区,读取

      线程同步的概念

      在多线程应用程序中,由于多个线程的存在,线程之间可能需要访问同一个变量。或一个线程需要等待另外一个线程完成某个操作后才能产生相应的动作。例如,在上一个例子中,工作线程产生随机的骰子点数,主线程读取骰子点数并显示,主线程需要等待工作线程产生一个新的骰子点数后再读取数据。在代码中我使用了信号与槽的机制,在产生新的骰子数之后通过信号通知主线程读取新的数据。

      如果不是用信号和槽机制,QDiceThread的 run() 函数要变为如下代码:

      void QDiceThread::run()
      {
          m_stop=false;//启动线程时令m_stop=false
          m_seq=0;
          qsrand(QTime::currentTime().msec());
      
          while(!m_stop)//循环主体
          {
              if (!m_paused)
              {
                  m_diceValue = qrand();
                  m_diceValue = (m_diceValue % 6) + 1;
                  m_seq++;
              }
              msleep(500); //线程休眠100ms
          }
      }
      
      

      那么 QTiceThread需要定义函数来返回 m_diceValue的值,如:

      int QDiceThread::diceValue () {return m_diceValue;}
      

      以便在主线程中调用此函数来读取骰子的点数。

      由于没有信号和槽的关联(信号和槽的关系类似硬件的中断与中断函数),主线程只能采用不断查询的方式主动查询是否由由新数据,并读取它。但是在主线程调用 diceValue()读取骰子点数时,工作线程可能正在执行 run()函数里修改 m_diceValue 值的语句,即:

      m_diceValue = qrand();
      m_diceValue = (m_diceValue % 6) + 1;
      m_seq++;
      

      而且这几条语句计算量过大,需要执行较长事件。执行这两条语句时不希望被主线程调用的 diceValue()中断,如果中断,则主线程得到的可能时错误值。

      这种情况下,这样的代码段时希望杯保护起来的,在执行过程中不能被其他线程打断,以保证计算结果的完整性,这就是线程同步的概念。

      在 Qt 中,由多个类可以实现线程同步的功能,包裹 QMutex, QMutexLocker, QReadWriteLock, QReadLocker, QWriteLocker, QWaitCondition, QSemaphore。下面将分别介绍这些类的用法。

      基于互斥量的线程同步

      QMutex和 QMutexLocker时基于互斥量的线程同步类,QMutex定义的实例是一个互斥量,QMutex主要提供三个函数。

      函数名 作用
      lock() 锁定互斥量,如果另外一个线程锁定了这个互斥量,他将阻塞执行知道其他线程解锁这个互斥量。
      unlock() 解锁一个互斥量,需要与 lock() 配对使用。
      tryLock() 试图锁定一个互斥量,如果成功锁定就返回 true;如果其他线程已经锁定了这个互斥量,就返沪 false,但不阻塞程序执行。

      使用这个互斥量,对 QDiceThread类重新定义,不采用信号和槽的机制,二十提供一个函数用于主线程读取。更改后的 QDiceThread类定义如下:

      class QDiceThread : public QThread
      {
          Q_OBJECT
      
      private:
          QMutex  mutex; //互斥量
      
          int     m_seq=0;//序号
          int     m_diceValue;
          bool    m_paused=true;
          bool    m_stop=false; //停止线程
      protected:
          void    run() Q_DECL_OVERRIDE;
      public:
          QDiceThread();
      
          void    diceBegin();//掷一次骰子
          void    diceEnd();//
          void    stopThread();
      
          bool    readValue(int *seq, int *diceValue); //用于主线程读取数据的函数
      };
      
      

      定义了函数 readValue(),用于外部线程读取骰子的次数的点数,传递参数采用的指针变量,一边一次读取两个数据。下面时 QDiceThread类中关键的 run()和 readValue()函数的实现代码:

      bool QDiceThread::readValue(int *seq, int *diceValue)
      {
          if (mutex.tryLock())
          {
              *seq=m_seq;
              *diceValue=m_diceValue;
              mutex.unlock();
              return true;
          }
          else
              return false;
      }
      
      void QDiceThread::run()
      {
          m_stop=false;//启动线程时令m_stop=false
          m_seq=0;
      //    qsrand(QTime::currentTime().msec());//随机数初始化,qsrand()过时了
      
          while(!m_stop)//循环主体
          {
              if (!m_paused)
              {
                  mutex.lock();
      //            m_diceValue=qrand(); //获取随机数,过时的函数
      //            m_diceValue=(m_diceValue % 6)+1;
                  m_diceValue= QRandomGenerator::global()->bounded(1,7);  //随机数[1,6]
                  m_seq++;
                  mutex.unlock();
              }
              msleep(500); //线程休眠100ms
          }
      }
      

      在 run() 函数中,对重新计算骰子点数和掷骰子次数的3行代码用互斥量 mutex的 lock() 和 umlock() 进行了保护,这部分代码的执行就不会被其他线程中断。注意,lock()与unlock()必须配对

      使用。在readValue() 函数中,用互斥量 mutex的 tryLock() 和 unlock() 进行了保护。如果 tryLock() 成功锁定互斥量,读取数值的两行代码执行时不会被中断,执行完后解锁;如果 tyLock() 锁定失败函数就立即返回,而不会等待。原理上,对于两个或多个线程可能会同时读或写的变量应该使用互斥量进行保护,例如QDiceThread中的变量 m_stop 和 m_paused,在 run()函数中读取这两个变量,要在 diceBegin()、diceEnd()和 stopThread() 函数里修改这些值,但是这3个函数都只有一条赋值语句,可以认为是原子操作,所以可以不用锁定保护。

      定义的互斥量 mutex相当于一个标牌,可以这样来理解互斥量:列车上的卫生间一次只能进一个人,当一个人尝试进入卫生间就是 lock(),如果有人占用,他就只能等待;等里面的人出来,腾出了卫生间是 unlock(),这个等待的人可以键入并锁在卫生间的门口,也就是 lock(),使用完卫生间之后他在出来就是 unclock()。

      QMutexLocker是另外一个简化了互斥量处理的类。QMutexLocker的构造函数接受一个互斥量作为参数并将其锁定,QMutexLocker的析构函数则将此互斥量解锁,所以 QMutexLocker实例变量的生存周期内的代码得到保护,自动进行互斥量锁定。例如下列代码:

      void QDiceThread::run()
      {
          m_stop=false;//启动线程时令m_stop=false
          m_seq=0;
      
          while(!m_stop)//循环主体
          {
              if (!m_paused)
              {
                  QMutexLocker locker(&mutex);
                  m_diceValue= QRandomGenerator::global()->bounded(1,7);  //随机数[1,6]
                  m_seq++;
              }
              msleep(500); //线程休眠100ms
          }
      }
      

      使用互斥量的方法的时候,在主程序中只能调用函数来不断读取数值。我们可以使用定时器来周期性地主动区读取骰子线程的数值。实例程序的窗口类主要定义如下(省略了一些系统生成的声明):

      class Dialog : public QDialog
      {
          Q_OBJECT
      
      private:
          int mSeq,mDiceValue;
      
          QDiceThread   threadA;
          QTimer  mTimer;//定时器
      protected:
          void    closeEvent(QCloseEvent *event);
      public:
          explicit Dialog(QWidget *parent = 0);
          ~Dialog();
      
      private slots:
          void    onthreadA_started();
          void    onthreadA_finished();
      
          void    onTimeOut(); //定期器处理槽函数
      
      private:
          Ui::Dialog *ui;
      };
      

      主要是增加了一个定时器 mTimer和其他事件溢出相应槽函数 onTimeOut(),在 Dialog 的构造函数中将 mTimer的 timeout 信号与此槽关联。

      connect(&mTimer,SIGNAL(timeout()),this,SLOT(onTimeOut()));
      

      onTimeOut()函数的主要功能是调用 threadA 的 readValue()函数读取数值。定时器的定时周期设置为 100ms,小于 threadA 产生一次新数据的周期(500ms),所以可能读出旧的数据,通过存储的投骰子的次数与读取的投骰子次数是否不同,判断是否为新数据。onTimeOut()函数的代码如下:

      void Dialog::onTimeOut()
      { //定时器到时处理槽函数
          int tmpSeq=0,tmpValue=0;
          bool  valid=threadA.readValue(&tmpSeq,&tmpValue); //读取数值
          if (valid && (tmpSeq!=mSeq)) //有效,并且是新数据
          {
              mSeq=tmpSeq;
              mDiceValue=tmpValue;
              QString  str=QString::asprintf("第 %d 次掷骰子,点数为:%d",mSeq,mDiceValue);
              ui->plainTextEdit->appendPlainText(str);
              QPixmap pic;
              QString filename=QString::asprintf(":/dice/images/d%d.jpg",mDiceValue);
              pic.load(filename);
              ui->LabPic->setPixmap(pic);
          }
      }
      

      窗口上几个按钮的代码如下(省略了按钮使能控制的代码):

      void Dialog::on_btnClear_clicked()
      {//清空文本
          ui->plainTextEdit->clear();
      }
      
      void Dialog::on_btnDiceEnd_clicked()
      {//暂停掷骰子
          threadA.diceEnd(); //
          mTimer.stop();//定时器暂停
      }
      
      void Dialog::on_btnDiceBegin_clicked()
      {//开始掷骰子
          threadA.diceBegin();
          mTimer.start(100); //定时器100读取一次数据
      }
      
      void Dialog::on_btnStopThread_clicked()
      {//结束线程
          threadA.stopThread();//结束线程的run()函数执行
          threadA.wait();//
      }
      
      void Dialog::on_btnStartThread_clicked()
      {//启动线程
          mSeq=0;
          threadA.start();
      }
      
      

      基于 QReadWriteLock 的线程同步

      使用互斥量时存在一个问题:每次只能由一个线程获得互斥量的权限。如果在一个程序中有多个线程读取某个变量,使用互斥量时必须排队。而实际上若只是读取一个变量,是可以让多个线程同时访问的,这样互斥量就会掉地程序的性能。

      例如,假设有一个数据采集程序,一个线程负责采集数据到缓冲区,一个线程负责读取缓冲区的数据并显示,另一个线程负责读取缓冲区的数据并保存到文件,代码如下:

      int buffer[100];
      QMutex mutex;
      void threadDAQ::run() {
          ...
          mutex.lock();
          get_data_and_write_in_buffer();	//数据写入 buffer
          mutex.unlock();
          ...
      }
      void threadShow::run() {
          ...
          mutex.lock();
          show_buffer(); //读取 buffer 里面的数据并显示
          mutex.unlock();
          ...
      }
      void threadSaveFile::run() {
          ...
          mutex.lock();
          Save_buffer_toFile(); // 读取 buffer 里的数据并保存到文件
          mutex.unlock();
          ...
      }
      

      数据缓冲区 buffer 互斥量 mutex都是全局变量,线程 threadDAQ将数据写到 buffer,线程 threadShow和 threadSaveFile只是读取 buffer,但是因为使用了互斥量,这 3 个线程任何时候都只能有一个线程可以访问 buffer。而实际上,threadShow和 threadSaveFile都只是读取 buffer 的数据,他们同时访问 buffer 是不会发生冲突的。

      Qt 提供了 QReadWriteLock类,他是基于读或写的模式进行代码锁定的,在多个线程读写一个共享数据时,可以解决上面所说的互斥量存在的问题。

      QReadWriteLock以读或写锁定的同步方法允许以读或写的方式保护一段代码,它可以允许多个线程以只读方式同步访问资源,但是只要有一个线程在以写方式访问资源时,其他线程就必须等待直到写操作结束。

      QReadWriteLock提供以下几个主要函数:

      函数名 作用
      lockForRead() 以只读方式锁定资源,如果有其他线程以写入方式锁定,这个函数会阻塞。
      lockForWrite() 以写入方式锁定资源,如果本线程或其他线程以读或写模式锁定资源,这个函数就阻塞。
      unlock() 解锁
      tryLockForRead() 是 lockForRead() 的非阻塞版本
      tryLockForWrite() 是 lockForWrite() 的非阻塞版本

      使用 QReadWriteLock,上面三个线程代码可以改写为如下的形式:

      int buffer[100];
      QReadWriteLock lock;
      void threadDAQ::run() {
          ...
          lock.lockForWrite();
          get_data_and_write_in_buffer();	//数据写入 buffer
          lock.unlock();
          ...
      }
      void threadShow::run() {
          ...
          lock.lockForRead();
          show_buffer(); //读取 buffer 里面的数据并显示
          lock.unlock();
          ...
      }
      void threadSaveFile::run() {
          ...
          lock.lockForRead();
          Save_buffer_toFile(); // 读取 buffer 里的数据并保存到文件
          lock.unlock();
          ...
      }
      

      这样的话,如果 threadDAQ没有加写锁,那么 threadShow 和 threadSaveFile 可以同时访问 buffer,否则都会被阻塞;如果 threadShow和 threadSaveFile都没有锁定,那么 threadDAQ能以写入方式锁定,否则就被阻塞。

      QReadLocker和 QWriteLocker是 QReadWriteLock的简便形式,如同 QMutexLocker是 QMutex的简便版本一样,使用方法如下:

      int buffer[100];
      QReadWriteLock lock;
      void threadDAQ::run() {
          ...
          QWriteLocker locker(&lock);
          get_data_and_write_in_buffer();	//数据写入 buffer
          lock.unlock();
          ...
      }
      void threadShow::run() {
          ...
          QReadLocker locker(&lock);
          show_buffer(); //读取 buffer 里面的数据并显示
          lock.unlock();
          ...
      }
      void threadSaveFile::run() {
          ...
          QReadLocker locker(&lock);
          Save_buffer_toFile(); // 读取 buffer 里的数据并保存到文件
          lock.unlock();
          ...
      }
      

      基于 QWaitCondition 的线程同步

      在多线程程序中,多个线程之间的同步实际上就是他们之间的协调问题。在上文的例子中,假设我们需要写满一个缓冲区才可以让读线程读取。前面采用互斥量和读写锁的方法都是对资源的锁定和解锁,避免同时访问资源时发生冲突。在一个线程解锁资源后,不能及时通知其他线程。

      QWaitCondition提供了另外一种改进的线程同步方法,QWaitCondition与 QMutex结合,可以使一个线程在满足一定条件时通知其他多个线程,使他们能及时做出相应,这样比只有互斥量效率高一些。QWaitCondition提供如下一些函数:

      函数名 作用
      wait(QMutex* lockedMutex) 解锁互斥量 lockedMutex,并阻塞等待唤醒条件,被唤醒后锁定 lockedMutex 并退出函数。
      wakeAll() 唤醒所有处于等待状态的线程,唤醒顺序不确定,由操作系统的调度策略决定。
      wakeOne() 唤醒一个处于等待状态的线程,唤醒哪个线程不确定,由操作系统调度策略决定。

      QWaitCondition一般用于“生产者/消费者”模型。“生产者”产生数据,“消费者”使用数据。实例代码的头文件如下:

      class QThreadProducer : public QThread
      {
          Q_OBJECT
      private:
          bool    m_stop=false; //停止线程
      protected:
          void    run() Q_DECL_OVERRIDE;
      public:
          QThreadProducer();
          void    stopThread();
      };
      
      
      class QThreadConsumer : public QThread
      {
          Q_OBJECT
      private:
          bool    m_stop=false; //停止线程
      protected:
          void    run() Q_DECL_OVERRIDE;
      public:
          QThreadConsumer();
          void    stopThread();
      signals:
          void    newValue(int seq,int diceValue);
      };
      

      QThreadProducer用于投骰子,但是去掉了开始和暂停功能,线程一启动就连续投骰子。QThreadConsumer用于读取投骰子的次数和点数,并用发射信号的方式把数据传递出去。下面是这两个类的实现代码主要部分:

      QMutex  mutex;
      QWaitCondition  newdataAvailable;
      void QThreadProducer::run()
      {
          m_stop=false;//启动线程时令m_stop=false
          seq=0;
      //    qsrand(QTime::currentTime().msec());//随机数初始化,qsrand()是过时的
      
          while(!m_stop)//循环主体
          {
              mutex.lock();
      //        diceValue=qrand(); //获取随机数,qrand()是过时的
      //        diceValue=(diceValue % 6)+1;
              diceValue= QRandomGenerator::global()->bounded(1,7);    //随机数[1,6]
              seq++;
              mutex.unlock();
      
              newdataAvailable.wakeAll();//唤醒所有线程,有新数据了
              msleep(500); //线程休眠100ms
          }
      }
      
      
      void QThreadConsumer::run()
      {
          m_stop=false;//启动线程时令m_stop=false
          while(!m_stop)//循环主体
          {
              mutex.lock();
              newdataAvailable.wait(&mutex);//会先解锁mutex,使其他线程可以使用mutex
              emit    newValue(seq,diceValue);
              mutex.unlock();
      //        msleep(100); //线程休眠100ms
          }
      
      }
      

      投骰子的次数和点数的变量定义为共享变量,这样两个线程都可以访问。定义了互斥量 mutex,定义了 QWaitCondition实例 newdataAvailable,表示有新数据可用了。

      QThreadProducer::run()函数负责每 500 毫秒产生一个新数据,新数据产生后通过等待条件唤醒所有等待的线程,即:

      newdataAvailable.wakeAll();
      

      QThreadConsumer::run()函数中使用循环,首先需要互斥量锁定,再执行下面的一条语句:

      newdataAvailable.wait(&mutex);
      

      这条语句首先会解锁 mutex,使其他线程可以使用 mutex,newdataAvailable 进入等待状态。当 QThreadProducer产生新数据使用 newdataAvailable.wakeAll() 唤醒所有线程后,newdataAvailable.wait(&mutex) 会再次锁定 mutex,然后退出阻塞状态,以执行后面的语句。

      所以,使用 QWaitCondition可以使 QThreadConsumer线程的执行过程进入等待状态。在 QThreadProducer线程满足条件后,唤醒 QThreadConsumer线程及时退出等待状态,继续执行后面的程序。下面通过 GUI 来显示线程工作情况和状态:

      QtThread线程同步和缓冲区设计

      窗口类的定义如下,省略了按钮槽函数等不重要部分:

      class Dialog : public QDialog
      {
          Q_OBJECT
      
      private:
          QThreadProducer   threadProducer;
          QThreadConsumer   threadConsumer;
      protected:
          void    closeEvent(QCloseEvent *event);
      public:
          explicit Dialog(QWidget *parent = 0);
          ~Dialog();
      
      private slots:
          void    onthreadA_started();
          void    onthreadA_finished();
      
          void    onthreadB_started();
          void    onthreadB_finished();
      
          void    onthreadB_newValue(int seq, int diceValue);
      
      private:
          Ui::Dialog *ui;
      };
      

      启动线程按钮的代码如下:

      void Dialog::on_btnStartThread_clicked()
      {//启动线程
          threadConsumer.start();
          threadProducer.start();
      
          ui->btnStartThread->setEnabled(false);
          ui->btnStopThread->setEnabled(true);
      }
      

      两个线程启动的先后顺序不可以调换顺序,应该先启动 threadConsumer,使其进入 wait状态,后启动 threadProducer,这样在 threadProducer里 wakeAll()时 threadConsumer就可以及时相应,否则会丢失第一次投骰子的数据。

      结束线程按钮的代码如下:

      void Dialog::on_btnStopThread_clicked()
      {//结束线程
          threadProducer.stopThread();//结束线程的run()函数执行
          threadProducer.wait();//
      
      //    threadConsumer.stopThread();//结束线程的run()函数执行
          threadConsumer.terminate(); //因为threadB可能处于等待状态,所以用terminate强制结束
          threadConsumer.wait();//
      
          ui->btnStartThread->setEnabled(true);
          ui->btnStopThread->setEnabled(false);
      }
      

      结束线程时,若按照上面的顺序先结束“生产者”线程,则必须使用 terminate()来强制结束 threadConsumer线程,因为 threadConsumer可能还处于条件等待的阻塞状态中,将无法正常结束线程。

      基于信号量的线程同步

      信号量的原理

      信号量时另一种限制对共享资源进行访问的线程同步机制,它与互斥量相似,但是有区别。一个互斥量只能被锁定一次,而信号量可以多次使用。信号量通常用来保护一定数量的相同资源,如数据采集时的双缓冲区。

      QSemaphore是实现信号量功能的类,它提供以下几个基本函数:

      函数名 作用
      acquire(int n) 尝试获取 n 个资源。如果没有那么多资源,线程将阻塞直到有 n 个资源可用。
      release(int n) 释放 n 个资源,如果信号量的资源已全部可用之后再 release(),就可以创建更多的资源,增加可用资源的个数。
      int available() 返回当前信号量可用的资源个数,这个数永远不可能为负数,如果为 0,就说明当前没有资源可用。
      bool tryAcquire(int n = 1) 尝试获取 n 个资源,不成功时不阻塞线程。

      下面这段代码可用说明 QSemaphore的几个函数的作用。

      QSemaphore wc(5); // 初始资源为5
      wc.acquire(4); // 使用了4个资源,还有1个
      wc.release(2); // 释放2个资源,还有三个可用
      wc.acquire(3); // 用了3个资源,还有0个可用
      wc.tryAcquire(1); // 返回false
      wc.acquire(); // 没有可用资源,阻塞
      

      双缓冲区数据采集和读取线程类设计

      信号量通常用来保护一定数量的相同的资源,如数据采集时的双缓冲区,适用于 Producer/Consumer模型。

      我们对这两个类的定义如下:

      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 是数据采集线程,例如在数据采集卡进行连续采集时,需要一个单独的线程将采集卡采集的数据读取到缓冲区内。
      • QThreadShows 是数据读取线程,用于读取已存满数据的缓冲区中的数据并传递给主线程显示,采用信号与槽机制与主线程交互。

      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
          }
          quit();
      }
      
      void QThreadShow::run()
      {
          m_stop=false;//启动线程时令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);//给主线程传递数据
          }
          quit();
      }
      

      在共享变量区定义了两个缓冲区 buffer1 和 buffer2,都是长度为 BufferSize 的数组。

      • curBuf 记录当前写入操作的缓冲区编号,其值只能是 1 或 2,表示 buffer1 或 buffer2,bufNo是累积的缓冲区个数编号,counter 是模拟采集数据的变量。
      • 信号量 emptyBufs 初始资源个数为 2,表示有 2 个空的缓冲区可用。
      • 信号量 fullBufs 初始化资源个数为 0,表示写满数据的缓冲区个数为 0。
      • QThreadDAQ::run()采用双缓冲区方式进行模拟数据采集,线程启动时初始化共享变量,特别的是使 emptyBufs的可用资源个数初始化为 2。
      • 在 while 循环体里,第一行语句 emptyBufs.acquire() 使信号量 empty.Bufs 获取一个资源,即获取一个空的缓冲区。用于数据缓存的有两个缓冲区,只要有一个空的换冲区,就可以向这个缓冲区写入数据。
      • 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,由主线程读取数据并显示。

      实际使用数据采集卡进行连续数据采集时,采集线程是不能停顿下来的,也就是说万一读取线程执行较慢,采集线程是不会等待的。所以实际情况下,读取线程的操作应该比采集线程快。

      QThreadDAQ 和 QThreadShow 的使用

      主窗口设计代码如下:

      class Dialog : public QDialog
      {
          Q_OBJECT
      
      private:
          QThreadDAQ   threadProducer;
          QThreadShow   threadConsumer;
      protected:
          void    closeEvent(QCloseEvent *event);
      public:
          explicit Dialog(QWidget *parent = 0);
          ~Dialog();
      
      private slots:
          void    onthreadA_started();
          void    onthreadA_finished();
      
          void    onthreadB_started();
          void    onthreadB_finished();
      
          void    onthreadB_newValue(int *data, int count, int bufNo);
      
      private:
          Ui::Dialog *ui;
      };
      

      这里定义了两个线程实例,threadProducer 和 threadConsumer。自定义了一个槽函数 onthreadB_newValue(),用于与 threadConsumer 的信号关联,在 Dialog 的构造函数里进行关联。

      connect(&threadConsumer,SIGNAL(newValue(int*,int,int)),
                  this,SLOT(onthreadB_newValue(int*,int,int)));
      

      槽函数 onthreadB_newValue() 的功能就是读取一个缓冲区里的数据并显示,代码如下:

      void Dialog::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 Dialog::on_btnStartThread_clicked()
      {//启动线程
          threadConsumer.start();
          threadProducer.start();
      
          ui->btnStartThread->setEnabled(false);
          ui->btnStopThread->setEnabled(true);
      }
      
      • 启动线程时,先启动 threadConsumer,再启动 threadProducer,否则可能丢失第一个缓冲区的数据。
      • 结束线程时,都采用 terminate()函数强制结束线程,因为两个线程之间有互锁的关系,若不使用 terminate()强制结束会出现线程无法结束的问题。

      运行结果

      QtThread线程同步和缓冲区设计

      从图中看出,没有丢失数据的情况出现,两个线程之间协调的很好,将 QThreadDAQ::run()函数中模拟采样率的延时时间调整为 2 毫秒也没有问题。

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

      版权声明:本文内容来自第三方投稿或授权转载,原文地址:https://lennlouis.blog.csdn.net/article/details/143632855,作者:小狮子安度因,版权归原作者所有。本网站转在其作品的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如因作品内容、版权等问题需要同本网站联系,请发邮件至ctyunbbs@chinatelecom.cn沟通。

      上一篇:Linux源码阅读笔记-USB设备驱动架构

      下一篇:Java中的反射机制与动态类操作

      相关文章

      2025-05-19 09:04:53

      【NetApp数据恢复】误操作导致NetApp存储的卷丢失,卷内虚拟机无法访问的数据恢复案例

      【NetApp数据恢复】误操作导致NetApp存储的卷丢失,卷内虚拟机无法访问的数据恢复案例

      2025-05-19 09:04:53
      存储 , 数据 , 数据恢复 , 解压
      2025-05-19 09:04:44

      js小题2:构造函数介绍与普通函数对比

      js小题2:构造函数介绍与普通函数对比

      2025-05-19 09:04:44
      new , 关键字 , 函数 , 对象 , 构造函数
      2025-05-19 09:04:30

      【Canvas技法】辐射式多道光影的实现

      【Canvas技法】辐射式多道光影的实现

      2025-05-19 09:04:30
      代码 , 函数 , 实现
      2025-05-19 09:04:22

      外设驱动库开发笔记54:外设库驱动设计改进的思考

      外设驱动库开发笔记54:外设库驱动设计改进的思考

      2025-05-19 09:04:22
      使用 , 函数 , 初始化 , 定义 , 对象
      2025-05-19 09:04:14

      C语言字符函数和字符串函数--(超全超详细)

      C语言字符函数和字符串函数--(超全超详细)

      2025-05-19 09:04:14
      函数 , 字符 , 字符串
      2025-05-19 09:04:14

      复杂度的OJ练习

      复杂度的OJ练习

      2025-05-19 09:04:14
      代码 , 复杂度 , 思路 , 数组 , 算法
      2025-05-16 09:15:24

      如何将一串数字用函数的方法倒过来(C语言)

      如何将一串数字用函数的方法倒过来(C语言)

      2025-05-16 09:15:24
      函数 , 数字 , 数组
      2025-05-16 09:15:17

      Linux系统基础-多线程超详细讲解(5)_单例模式与线程池

      Linux系统基础-多线程超详细讲解(5)_单例模式与线程池

      2025-05-16 09:15:17
      单例 , 线程 , 队列
      2025-05-16 09:15:10

      画图时使用的函数和一些错误处理

      画图时使用的函数和一些错误处理

      2025-05-16 09:15:10
      数据
      2025-05-14 10:33:31

      【数据结构】第一章——绪论(2)

      【数据结构】第一章——绪论(2)

      2025-05-14 10:33:31
      函数 , 实现 , 打印 , 理解 , 算法 , 输入 , 输出
      查看更多
      推荐标签

      作者介绍

      天翼云小翼
      天翼云用户

      文章

      33561

      阅读量

      5232098

      查看更多

      最新文章

      【Canvas技法】辐射式多道光影的实现

      2025-05-19 09:04:30

      外设驱动库开发笔记54:外设库驱动设计改进的思考

      2025-05-19 09:04:22

      C语言字符函数和字符串函数--(超全超详细)

      2025-05-19 09:04:14

      复杂度的OJ练习

      2025-05-19 09:04:14

      如何将一串数字用函数的方法倒过来(C语言)

      2025-05-16 09:15:24

      Linux系统基础-多线程超详细讲解(5)_单例模式与线程池

      2025-05-16 09:15:17

      查看更多

      热门文章

      Java线程同步synchronized wait notifyAll

      2023-04-18 14:15:05

      Python|斐波那契数列

      2023-02-27 10:01:21

      游戏编程之十一 图像页CPICPAGE介绍

      2022-11-28 01:25:04

      PHP:将list列表转为tree树形数据

      2023-02-28 08:23:26

      Python 函数调用父类详解

      2023-04-23 09:44:31

      数据结构与算法之七 栈

      2022-11-17 12:37:20

      查看更多

      热门标签

      java Java python 编程开发 代码 开发语言 算法 线程 Python html 数组 C++ 元素 javascript c++
      查看更多

      相关产品

      弹性云主机

      随时自助获取、弹性伸缩的云服务器资源

      天翼云电脑(公众版)

      便捷、安全、高效的云电脑服务

      对象存储

      高品质、低成本的云上存储服务

      云硬盘

      为云上计算资源提供持久性块存储

      查看更多

      随机文章

      Redis 新特性篇:多线程模型解读

      【Python】使用urllib模块实现网页内容读取,读取指定URL的网页内容

      Go Web 编程入门:验证器

      python知识点总结-match、search、group、groups、findall的区别

      Linux源码学习笔记01-Linux内核源码结构

      算法题:剑指 Offer 30. 包含min函数的栈(题目+思路+代码+注释)时空 O(1) O(1) 0ms击败94%、69%用户

      • 7*24小时售后
      • 无忧退款
      • 免费备案
      • 专家服务
      售前咨询热线
      400-810-9889转1
      关注天翼云
      • 旗舰店
      • 天翼云APP
      • 天翼云微信公众号
      服务与支持
      • 备案中心
      • 售前咨询
      • 智能客服
      • 自助服务
      • 工单管理
      • 客户公告
      • 涉诈举报
      账户管理
      • 管理中心
      • 订单管理
      • 余额管理
      • 发票管理
      • 充值汇款
      • 续费管理
      快速入口
      • 天翼云旗舰店
      • 文档中心
      • 最新活动
      • 免费试用
      • 信任中心
      • 天翼云学堂
      云网生态
      • 甄选商城
      • 渠道合作
      • 云市场合作
      了解天翼云
      • 关于天翼云
      • 天翼云APP
      • 服务案例
      • 新闻资讯
      • 联系我们
      热门产品
      • 云电脑
      • 弹性云主机
      • 云电脑政企版
      • 天翼云手机
      • 云数据库
      • 对象存储
      • 云硬盘
      • Web应用防火墙
      • 服务器安全卫士
      • CDN加速
      热门推荐
      • 云服务备份
      • 边缘安全加速平台
      • 全站加速
      • 安全加速
      • 云服务器
      • 云主机
      • 智能边缘云
      • 应用编排服务
      • 微服务引擎
      • 共享流量包
      更多推荐
      • web应用防火墙
      • 密钥管理
      • 等保咨询
      • 安全专区
      • 应用运维管理
      • 云日志服务
      • 文档数据库服务
      • 云搜索服务
      • 数据湖探索
      • 数据仓库服务
      友情链接
      • 中国电信集团
      • 189邮箱
      • 天翼企业云盘
      • 天翼云盘
      ©2025 天翼云科技有限公司版权所有 增值电信业务经营许可证A2.B1.B2-20090001
      公司地址:北京市东城区青龙胡同甲1号、3号2幢2层205-32室
      • 用户协议
      • 隐私政策
      • 个人信息保护
      • 法律声明
      备案 京公网安备11010802043424号 京ICP备 2021034386号