首页 > 其他分享 >QtThread线程同步和缓冲区设计

QtThread线程同步和缓冲区设计

时间:2024-11-08 18:44:33浏览次数:3  
标签:run int void 互斥 QtThread 线程 缓冲区

线程同步的概念

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

如果不是用信号和槽机制,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。下面将分别介绍这些类的用法。

基于互斥量的线程同步

QMutexQMutexLocker时基于互斥量的线程同步类,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行代码用互斥量 mutexlock()umlock() 进行了保护,这部分代码的执行就不会被其他线程中断。注意,lock()与unlock()必须配对

使用。在readValue() 函数中,用互斥量 mutextryLock()unlock() 进行了保护。如果 tryLock() 成功锁定互斥量,读取数值的两行代码执行时不会被中断,执行完后解锁;如果 tyLock() 锁定失败函数就立即返回,而不会等待。原理上,对于两个或多个线程可能会同时读或写的变量应该使用互斥量进行保护,例如QDiceThread中的变量 m_stopm_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,线程 threadShowthreadSaveFile只是读取 buffer,但是因为使用了互斥量,这 3 个线程任何时候都只能有一个线程可以访问 buffer。而实际上,threadShowthreadSaveFile都只是读取 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没有加写锁,那么 threadShowthreadSaveFile 可以同时访问 buffer,否则都会被阻塞;如果 threadShowthreadSaveFile都没有锁定,那么 threadDAQ能以写入方式锁定,否则就被阻塞。

QReadLockerQWriteLockerQReadWriteLock的简便形式,如同 QMutexLockerQMutex的简便版本一样,使用方法如下:

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提供了另外一种改进的线程同步方法,QWaitConditionQMutex结合,可以使一个线程在满足一定条件时通知其他多个线程,使他们能及时做出相应,这样比只有互斥量效率高一些。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 来显示线程工作情况和状态:

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

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,这样在 threadProducerwakeAll()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 是数据读取线程,用于读取已存满数据的缓冲区中的数据并传递给主线程显示,采用信号与槽机制与主线程交互。

QThreadDAQQThreadShow的主要功能代码如下:

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()强制结束会出现线程无法结束的问题。

运行结果

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

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

标签:run,int,void,互斥,QtThread,线程,缓冲区
From: https://blog.csdn.net/H520xcodenodev/article/details/143632855

相关文章

  • Jmeter关联处理-跨越线程组的传值
    一、线程组1提取要传递的值设置全局变量,变量值在函数助手setProperty中设置:添加BeanShell取样器BeanShell取样器中设置要使用的全局变量:二、线程组2获取全局变量通过函数助手property获取:获取的全局变量,写入请求中三、总结:‌在JMeter中跨越线程组传值主要有以下......
  • 进程 线程
    1.举例进程,能够完成多任务,比如在一台电脑上能够同时运行多个QQ线程,能够完成多任务,比如一个QQ中的多个聊天窗口2.定义的不同进程是系统进行资源分配和调度的一个独立单位.线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自......
  • python中多线程和多进程的应用
    选择指南总结I/O密集型任务:使用多线程(ThreadPoolExecutor)。例如,爬虫抓取数据、从数据库或API获取信息、读取/写入文件等。计算密集型任务:使用多进程(multiprocessing.Pool)。例如,进行大量数据计算、数值分析等。混合型任务:可以结合多线程和多进程。例如,先用线程处理I/O......
  • python多线程:控制线程数量
    python多线程:控制线程数量  https://www.cnblogs.com/hanmk/p/12990017.html使用线程池  https://zhuanlan.zhihu.com/p/6278539371.自定义线程池1importthreading2importQueue3importtime45queue=Queue.Queue()678defput_data_in_qu......
  • 并发编程(6)——future、promise、async,线程池
    六、day6今天学习如何使用std::future、std::async、std::promise。主要内容包括:参考:https://llfc.club/category?catid=225RaiVNI8pFDD5L4m807g7ZwmF#!aid/2Agk6II6SsiG8DwPawfXHsP4bUThttps://github.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/blob/main/md......
  • 并发编程/6种线程池设计图/1大线程池标准设计与执行规范/2种线程池管理设计(全面篇)
    在现代多核处理器时代,线程池成为了并发编程中不可或缺的工具,它不仅提高了程序性能,还简化了线程管理。线程池允许我们重用有限数量的线程来执行大量任务,从而减少了线程创建和销毁的开销。Java中的ExecutorService接口及其实现类,如FixedThreadPool、SingleThreadExecutor、Ca......
  • 线程的概念、作用和属性
    线程的概念、作用和属性线程的概念理解:线程可视作“轻量级进程”。线程是一个基本的CPU执行单元,也是程序执行流的最小单位。引入线程之后,不仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以并发处理各种任务(如QQ视频、文......
  • 线程的实现方式和多线程模型
    线程的实现方式和多线程模型‍​​‍一、线程的实现方式(一)用户级线程​​‍(二)内核级线程​​‍二、多线程模型在支持内核级线程的系统中,根据用户级线程和内核级线程的映射关系,可以划分不同的多线程模型(一)一对一模型​​‍(二)多对一模型和上面提到的用户级线程的......
  • 两个线程交替写1~100
     packageTest;publicclassPrintNumber{privateintstatus=1,cnt=1;synchronizedvoidprint_odd(){while(cnt<100){while(status==2){try{this.wait();}......
  • Java并发编程 --- 线程安全
    为什么会有线程安全问题?为什么会存在线程安全问题呢?那我们先来探究一下JMM(Java内存模型)线程与JMM每个线程都有自己的工作内存,它会存储主内存中变量的Copy值,再对变量进行操作的时候,也是操作工作内存中变量的Copy值。当线程Dead(生命周期结束)时,才会将自己工作内存中的数据同......