线程同步的概念
在多线程应用程序中,由于多个线程的存在,线程之间可能需要访问同一个变量。或一个线程需要等待另外一个线程完成某个操作后才能产生相应的动作。例如,在上一个例子中,工作线程产生随机的骰子点数,主线程读取骰子点数并显示,主线程需要等待工作线程产生一个新的骰子点数后再读取数据。在代码中我使用了信号与槽的机制,在产生新的骰子数之后通过信号通知主线程读取新的数据。
如果不是用信号和槽机制,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 来显示线程工作情况和状态:
窗口类的定义如下,省略了按钮槽函数等不重要部分:
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()
强制结束会出现线程无法结束的问题。
运行结果
从图中看出,没有丢失数据的情况出现,两个线程之间协调的很好,将 QThreadDAQ::run()
函数中模拟采样率的延时时间调整为 2 毫秒也没有问题。
实际的数据采集中,要保证不丢失缓冲区或数据点,数据读取线程的速度必须快过数据写入缓冲区的线程的速度。
标签:run,int,void,互斥,QtThread,线程,缓冲区 From: https://blog.csdn.net/H520xcodenodev/article/details/143632855