相较于基于互斥锁的线程同步,基于信号量的线程同步可以控制同时访问某个资源的线程数量,他的机制就是给资源前面加上一个计数器,允许多少线程同时访问,当有一个线程访问的时候,计数器-1,当线程访问完了以后计数器+1。
就比如有一个资源,给他设置他有两个缓冲区来生产数组,有一个输出线程,一个采集线程,第一个缓冲区是空的,开始采集线程,采集中+1,空buf-1,当第一个缓冲区采集满了以后,采集中-1,满buf+1,输出线程在满buf不为0的时候就会开始工作,满buf-1,输出中+1,这时候缓冲区1是满的,我们可以开启缓冲区2来执行采集任务,方式和上面一样。
QSemaphore 是实现信号量功能的类,他有以下函数:
acquire(int n) | 尝试获取n个资源,如果不够就堵塞直到有n个资源可用 |
release(int n) | 释放资源,如果所有资源可用,可以扩充资源总数 |
int available() | 返回当前信号量可用资源个数 |
bool tryacquire(int n=1) | 尝试获取n个资源,不够时不会阻塞线程 |
接下来是基于信号量的线程同步实例。
这个实例和之前一样是一个掷色子的程序,这里我们和上个图一样的结构,设置两个缓冲区
int buffer1[BUF_SIZE];
int buffer2[BUF_SIZE];
int curBufNum=1; //当前正在写入的缓冲区编号
int bufSeq=0; //缓冲区序号
BUF_SIZE是定义的全局常量,我给的是10,代表缓冲区长度。有两个线程
//TDaqThread 数据采集线程
class TDaqThread : public QThread
{
Q_OBJECT
protected:
bool m_stop=false;
void run(); //线程的任务函数
public:
explicit TDaqThread(QObject *parent = nullptr);
void stopThread(); //结束线程run()函数的运行
};
//TProcessThread 数据处理线程
class TProcessThread : public QThread
{
Q_OBJECT
protected:
bool m_stop=false;
void run(); //线程的任务函数
public:
explicit TProcessThread(QObject *parent = nullptr);
void stopThread(); //结束线程run()函数的运行
signals:
void dataAvailable(int bufferSeq, int* bufferData, int pointCount);
};
两个信号量代表空和满的信号量,初始状态下两个缓冲区都是空的
QSemaphore emptyBufs(2); //信号量,空的缓冲区个数,初始资源个数为2
QSemaphore fullBufs; //信号量,满的缓冲区个数,初始资源为0
接下来是重载两个线程的run()函数
数据采集线程是在缓冲区生成数据的线程,在启动线程时会初始化全局变量,使信号量emptybufs的值为2,在while里面获取一个空的缓冲区就可以开始进行收集了,将*buf指向缓冲区的第一个位置,在for循环里面将buf解引用,把counter赋值给他,然后buf++,counter++,将指针挪到下一个位置赋值,这样也可以观察缓冲区赋值是否连续,当这一个缓冲区赋值满了以后就切换缓冲区,调用fullbufs的release()使fullbufs释放资源并且+1,处理数据的线程就可以来处理这个缓冲区了。(*这里有个问题需要注意!!!处理数据的线程执行时间要比收集数据的短,以确保缓冲区或者数据点不会丢失,下面每次赋值后都让他休息10ms就是让收集数据的线程慢点*)
void TDaqThread::run()
{
curBufNum=1; //当前正在写入的缓冲区编号
bufSeq=0; //第几组缓冲区数据
int counter=0; //模拟数据
int n=emptyBufs.available();
if (n<2) //保证线程启动时emptyBufs.available()==2
emptyBufs.release(2-n);
m_stop =false;
while(!m_stop)
{
emptyBufs.acquire(); //获取一个空的缓冲区
int *buf = curBufNum==1? buffer1:buffer2; //设置当前缓冲区指针
for(int i=0; i<BUF_SIZE; i++) //产生一个缓冲区的数据
{
*buf=counter;
buf++;
counter++;
msleep(10);
}
bufSeq++; //缓冲区序号
curBufNum = curBufNum==1? 2:1; //切换当前写入缓冲区编号
fullBufs.release(); //fullBufs释放1个资源,有了一个满的缓冲区
}
}
当fullbufs>0的时候,就代表有一个满的缓冲区,需要将fullbufs初始化为0,线程调用tryAcquire来尝试获取一个缓冲区,等他50s,如果返回为true,将bufferfull指向获取的已满缓冲区,用bufData[i]遍历获取缓冲区中的数值,获取完后这个缓冲区就变为空的了,调用emptybufs的release()释放资源 并信号量emptybufs+1,将处理缓冲的组数,缓冲区数据以及数据点个数通过信号发送出去。
void TProcessThread::run()
{
int n=fullBufs.available();
if (n>0)
fullBufs.acquire(n); //将fullBufs可用资源个数初始化为0
int bufData[BUF_SIZE];
m_stop =false;
while(!m_stop)
if (fullBufs.tryAcquire(1,50)) //尝试获取1个资源,最多等待50ms
{
int *bufferFull= curBufNum==1? buffer2:buffer1; //获取已满缓冲区的指针
for(int i=0; i<BUF_SIZE; i++, bufferFull++) //模拟数据处理
bufData[i]=*bufferFull;
emptyBufs.release(); //emptyBufs释放一个资源,可用空缓冲区个数加1
int pointCount=BUF_SIZE;
emit dataAvailable(bufSeq, bufData,pointCount); //发射信号
}
}
线程的started和finished是私有信号,不能主动触发,所以他的槽函数要自己写一下
void MainWindow::do_threadA_started()
{//与线程的started()信号关联
ui->statusbar->showMessage("Thread状态:thread started");
ui->actThread_Run->setEnabled(false);
ui->actThread_Quit->setEnabled(true);
}
void MainWindow::do_threadA_finished()
{//与线程的finished()信号关联
ui->statusbar->showMessage("Thread状态:thread finished");
ui->actThread_Run->setEnabled(true);
ui->actThread_Quit->setEnabled(false);
}
do_readbuffer槽函数是将获取出来的数值打印出来,和数据处理线程的dataAvilable信号关联
void MainWindow::do_readBuffer(int bufferSeq, int *bufferData, int pointCount)
{
QString str=QString::asprintf("第 %d 个缓冲区:",bufferSeq);
for (int i=0; i<pointCount; i++)
{
str=str+QString::asprintf("%d, ",*bufferData);
bufferData++;
}
str=str+'\n';
ui->plainTextEdit->appendPlainText(str);
}
另外由于两个线程之间因为信号量存在耦合,结束线程不可以使用QThread::terminate强制结束,否则重新启动线程会出错,这里写了一个stopthread函数,触发的时候把m_stop赋值为true,线程的while的条件不满足,线程就会停止,重写closeevent的时候调用它就行了。