多线程学习之单消费者单生产者(ringbuffer)

写在前面

高速相机的实时采集存储和网络中的大数量传输有共同之处,核心部分就是利用生产者消费者模型完成高速数据传输或存储。关于图像数据的获取,由于高速相机提供的sdk利用缓冲区事件进行回调去调用专用线程,基本思路就是在专用线程中完成存储功能。

目标帧率500FPS
C写入bmp文件耗时4ms左右

基于以上目标,平均需要在2ms左右完成一张图像的存储。因此需要对C写文件的过程进行拆分(创建文件,写入数据,关闭文件)。将创建文件和写入数据交给生产者线程完成,关闭文件句柄交给消费者线程完成。其中涉及到FIFO队列的设计,在这里整理一下。

FIFO队列——环形缓冲区RingBuffer

FIFO队列,从本质上来说是一个队列,满足先进先出的特点。在大数据量高并发的应用场景中,利用FIFO作为缓冲队列是很常见的应用。与一般的队列相比,RingBuffer的实际物理地址在初始化后就不再改变,进行循环使用,即循环队列。对于生产者消费者线程来说,FIFO也就是临界资源,需要我们去进行互斥的访问,即对于同一个内存单元,生产者和消费者不可以同时访问。
FIFO数据结构的基本构成如下所示。

struct ringbuffer{
	char* buffer;	//缓冲内存
	int size;		//缓冲区大小
	int front;		//读者索引,即消费者索引
	int rear;		//写者索引,即生产者线程
	std::mutex mt;  //互斥量
	condition_varaible cond;	//条件变量
};

由于生产者线程和消费者线程在速率上往往存在差异,而两者处于一个队列通道内,需要保证对于同一个内存单元的读写操作的互斥进行。从逻辑上来说,对于队列,发生互斥的访问的情况有两种:
1、缓冲区为空,消费者线程追上了生产者线程
2、缓冲区为满,生产者线程追上了消费者线程
即我们常说的读写者问题(单生产者消费者模型)。为了内存操作的连续性,我们利用顺序存储来模拟环形队列,利用指针索引的取模运算来循环使用初始化后的缓冲内存。

为了避免只有一个元素时,队头和队尾的重合,引入front指针指向对头元素,rear指针指向队尾元素的下一个位置(待生产位置)。因此从逻辑上来说,当front==rear时,队列为空。

当rear指针到达物理内存尾部时,此时如何继续入队,会出现“假溢出”现象(为什么说假?别忘了,另一个线程在向后移动front指针,从队列头部到front之间的缓冲区已经时空的了)。因此,将rear移动到队列头部,重新开始计数,这里的操作就是进行取模运算。同理,当front到达尾部时,也一样处理。

然而,问题又来了,如果出现互斥2的情形,此时队列为满,但是front==rear,与空队列重合了。解决办法很简答,有两种:
A、设置一个标志变量,flag,当front==rear && flag==true时,队列为空,当front==rear && flag==false时,队列为满
B、在环形队列中,保留一个元素空间,即当队列为满时,数组中还有一个空闲单元
常用的方法是B。由于rear和front的大小关系会随buffer的工作而改变,且环形队列在逻辑上被认为首尾相接,因此,当队列满时,虽然从物理上看,两者差距为1,逻辑上相差了整整一圈。可以得出,队列满的条件为(reat+1)%size==front

FILE文件队列的有锁队列设计

FIFO的朴素实现方法就是利用生产者-消费者模型中典型的互斥量+条件变量实现两个线程的同步,即只允许一个线程占有资源锁。在FILE-RingBuffer的设计中,本文将创建文件和数据写入作为生产者线程,文件句柄关闭作为消费者线程。在仿真实验中设定缓冲区大小为64,连续存储1000张2304*1720大小的bmp图像。

生产者线程
当生产者获取到数据后,利用unique_lock对互斥量上锁,如果此时缓冲区为满,则等待消费者线程释放锁资源。获取到锁资源后,开始文件的创建和写入,之后更新生产者线程的索引,通过条件变量去通知消费者线程,释放锁资源。

void OpenWrite() {//开文件、写文件线程
	unsigned char* buf = NULL;
	printf("Start Open and Write %d frames...\n", N);

	for (int i = 0; i < N; ++i) {
		buf = img->imageData;
		std::unique_lock<std::mutex> lk(diskThread.mt);
		while ((diskThread.openPos + 1) % BUFFER_NUM == diskThread.closePos) {
			std::cout << "Resourse is full" << std::endl;
			diskThread.not_full.wait(lk);
		}
		diskThread.fileBuffer[diskThread.openPos] = creatFile(("D:\\disktest\\test" + std::to_string(i) + ".bmp").c_str());
		FILE* pfile = diskThread.fileBuffer[diskThread.openPos];
		fwrite(&fileType, sizeof(unsigned short), 1, pfile);
		//文件头信息写入
		fwrite(&bmpFileHeader, sizeof(BmpFileHeader), 1, pfile);
		//位图头信息写入
		fwrite(&bmpInfoHeader, sizeof(BmpInfoHeader), 1, pfile);
		//调色板写入
		fwrite(quad, sizeof(RgbQuad), 256, pfile);
		//数据写入
		fwrite(img->imageData, sizeof(unsigned char), picSize, pfile);
		diskThread.openPos = (diskThread.openPos + 1) % BUFFER_NUM;
		diskThread.not_empty.notify_all();
		std::cout << "Open.." << i << "..file" << std::endl;
	}
	diskThread.not_empty.notify_all();
}

消费者线程
逻辑和生产者线程其实相同,当文件队列为空,等着呗,等着资源被生产者释放呗。关闭文件后,更新索引,同样通过条件变量通知生产者。

void Close() {//关文件线程
	bool exit = false;
	while (1) {
		if (diskThread.count < N) {
			std::unique_lock<std::mutex> lk(diskThread.mt);
			while (diskThread.closePos == diskThread.openPos) {
				std::cout << "Resource is empty" << std::endl;
				diskThread.not_empty.wait(lk);
			}
			FILE* pfile = diskThread.fileBuffer[diskThread.closePos];
			fclose(pfile);
			diskThread.closePos = (diskThread.closePos + 1) % BUFFER_NUM;
			++(diskThread.count);
			diskThread.not_full.notify_all();
			std::cout << "Close File..." << diskThread.count << std::endl;
		}
		else
			exit = true;
		if (exit) break;
	}
	std::cout << "Exit closing thread: " << std::endl;
}

FILE文件队列的无锁队列设计

对于单生产者单消费者模型来说,并不一定需要加锁才能保证。在环形队列中,真正互斥的就是front和rear指针不能重合,即队列满时不能进行生产,队列空时不能进行消费。因此,只要保证队列满时生产者线程不进行写入操作,等待队列中出现空位,队列空时消费者线程不进行读出等待队列中出现新的数据即可。

class myRingBuffer {
public:
	myRingBuffer(int len) : front(0), rear(0), length(len) {
		filePtr = (FILE**)malloc(sizeof(FILE*) * len);
	}

	~myRingBuffer() {
		free((void*)filePtr);
	}

	bool isEmpty() {
		return front == rear;
	}

	bool isFull() {
		return (rear + 1) % length == front;
	}

	int size() {
		return this->length;
	}

	FILE** filePtr;
	int front;
	int rear;

private:
	int length;
};

无锁生产者

void FileInput() {
	int index = 0;
	unsigned char* buf = NULL;
	printf("Start Open and Write %d frames...\n", N);

	while(1) {
		if (filebuffer.isFull()) {
			std::cout << "Buffer is full" << std::endl;
			continue;
		}

		buf = img->imageData;
		filebuffer.filePtr[filebuffer.rear] = creatFile(("D:\\disktest\\test" + std::to_string(index) + ".bmp").c_str());
		FILE* pfile = filebuffer.filePtr[filebuffer.rear];
		fwrite(&fileType, sizeof(unsigned short), 1, pfile);
		//文件头信息写入
		fwrite(&bmpFileHeader, sizeof(BmpFileHeader), 1, pfile);
		//位图头信息写入
		fwrite(&bmpInfoHeader, sizeof(BmpInfoHeader), 1, pfile);
		//调色板写入
		fwrite(quad, sizeof(RgbQuad), 256, pfile);
		//数据写入
		fwrite(img->imageData, sizeof(unsigned char), picSize, pfile);
		filebuffer.rear = (filebuffer.rear + 1) % BUFFER_NUM;
		std::cout << "Open.." << index++ << "..file" << std::endl;
		if (index == N)
			break;
	}
	printf("Stop storing\n");
}

无锁消费者

void FileClose() {
	int index = 0;
	bool exit = false;
	while (!exit) {
		if (filebuffer.isEmpty()) {
			std::cout << "Buffer is empty" << std::endl;
			continue;
		}

		FILE* pfile = filebuffer.filePtr[filebuffer.front];
		fclose(pfile);
		filebuffer.front = (filebuffer.front + 1) % BUFFER_NUM;
		++index;
		if (index == N)
			exit = true;
	}
	std::cout << "Exit closing thread: " << std::endl;
}

仿真实验结果

利用两种队列对1000张图像进行写入存储测试,计算NVME硬盘(Intel 750 Serials 1.2TB)的写入速度。

写入方式速度(GB/s)
单线程串行0.8777
有锁队列0.7325
无锁队列1.0481

高速相机250FPS下实测

利用高速相机在250FPS下对单个硬盘进行存储测试,分别测试采集10s、30s、60s。

采集时间/s图像数量
102498
307497
6014996(不稳定,有时候会丢帧比较多)
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值