利用线程与环形缓冲区实现对数据的异步存储
一、流程图
- 数据流首先,进入缓存至环形缓冲区,当缓冲区中一个缓存块被填满后,释放信号量给线程;
- 线程被唤醒,从缓冲区中取出一个缓存块的数据,并写入文件。
以上,就是2个主要过程,如此循环往复。当然还有很多细节没有介绍,诸如,缓存块读、写指针的控制等。
二、FileSave文件异步存储类
以上所讲过程,全部封装到了FileSave类中。
数据推送接口,RealUCharPort.h
#ifndef REALUCHARPORT_H
#define REALUCHARPORT_H
#pragma once
class RealUCharPort
{
public:
virtual void pushPacket(unsigned char* pPkt,unsigned int iLen)=0;
};
#endif // REALUCHARPORT_H
FileSave.h
#ifndef FILESAVE_H
#define FILESAVE_H
#include <stdio.h>
#include "RealUCharPort.h"
#include "Mutex.h"
// 最大缓存块个数,以512KB为一个缓存块
#define BUFBLOCKNUM_MAX 10
// 默认有效缓存块个数,以512KB为一个缓存块
#define BUFBLOCKNUM_DEF 6
// 一个缓存块的大小
#define BLOCKSIZE (1 * 512 * 1024)
class FileSave : public RealUCharPort
{
public:
// 构造函数
FileSave();
// 虚构函数
virtual ~FileSave(void);
// 开始记录
bool Start(const char* FileName, unsigned int BlockNum = BUFBLOCKNUM_DEF);
// 停止记录
bool Stop();
// 接收数据函数
void pushPacket(unsigned char* pPkt,unsigned int iLen);
// 保存数据线程
void SaveData();
private:
// 是否开始记录
bool m_bStart;
// 接收缓冲区
unsigned char* m_RxBuf[BUFBLOCKNUM_MAX];
unsigned int m_RxLen[BUFBLOCKNUM_MAX];
// 读、写指针
unsigned int m_iWr_Block_Ptr;
unsigned int m_iWr_Ptr;
unsigned int m_iRd_Block_Ptr;
// 0-数据到达通知信号量句柄
// 1-通知线程退出信号量句柄
HANDLE m_hDataReady[2];
// 存储线程句柄
HANDLE m_hSaveThread;
// 文件
Mutex m_mFileLock;
FILE* m_pFile;
// 实际有效块数
unsigned int m_iBufBlockSize;
Mutex m_mBufLock;
char* _fileBuf;
};
#endif // FILESAVE_H
FileSave.cpp
#include "FileSave.h"
#include <QDebug>
#include <QMessageBox>
DWORD WINAPI SaveDataThread(LPVOID lpParameter)
{
FileSave* pSave = (FileSave*)lpParameter;
pSave->SaveData();
return(0);
}
FileSave::FileSave()
: m_pFile(NULL),
m_bStart(false),
m_iBufBlockSize(0),
m_iWr_Block_Ptr(0),
m_iWr_Ptr(0),
m_iRd_Block_Ptr(0),
_fileBuf(NULL)
{
//缓冲区初始化
for(int i = 0; i < BUFBLOCKNUM_MAX; i++)
{
m_RxBuf[i] = NULL;
m_RxLen[i] = 0;
}
//初始化句柄
m_hDataReady[0] = NULL;
m_hDataReady[1] = NULL;
m_hSaveThread = NULL;
//创建信号量
m_hDataReady[0] = CreateSemaphore(NULL, 0, BUFBLOCKNUM_MAX, NULL);
if(m_hDataReady[0] == NULL)
{
return;
}
m_hDataReady[1] = CreateSemaphore(NULL, 0, 1, NULL);
if(m_hDataReady[1] == NULL)
{
CloseHandle(m_hDataReady[0]);
m_hDataReady[0] = NULL;
return;
}
//创建一个线程用于保存文件
m_hSaveThread = CreateThread(NULL, 0, SaveDataThread, this, 0, NULL);
if(m_hSaveThread == NULL)
{
CloseHandle(m_hDataReady[0]);
m_hDataReady[0] = NULL;
CloseHandle(m_hDataReady[1]);
m_hDataReady[1] = NULL;
return;
}
}
FileSave::~FileSave(void)
{
if(m_hSaveThread != NULL)
{
//发出线程退出通知事件
ReleaseSemaphore(m_hDataReady[1], 1, NULL);
//等待线程正常退出(最多等待3000ms)
if(WAIT_TIMEOUT == WaitForSingleObject(m_hSaveThread, 3000))
{
TerminateThread(m_hSaveThread, 0); // 如果线程超时未退出,强制终止线程
}
//关闭句柄
CloseHandle(m_hDataReady[0]);
m_hDataReady[0] = NULL;
CloseHandle(m_hDataReady[1]);
m_hDataReady[1] = NULL;
CloseHandle(m_hSaveThread);
m_hSaveThread = NULL;
}
//关闭文件
if(m_bStart)
{
Stop();
m_bStart = false;
}
}
bool FileSave::Start(const char* FileName, unsigned int BlockNum)
{
//已经开始了,不得再次开始
if(m_bStart)
return false;
if(FileName == NULL)
return false;
//打开文件
m_pFile = fopen(FileName, "wb+");
if (m_pFile == NULL)
return false;
//申请缓冲空间
try
{
_fileBuf = new char[BLOCKSIZE];
for(unsigned int i = 0; i < BlockNum; i++)
{
m_RxBuf[i] = new unsigned char[BLOCKSIZE];
m_RxLen[i] = 0;
}
}
catch(...)
{
for(unsigned int i = 0; i < BlockNum; i++)
{
if (m_RxBuf[i] != NULL)
{
delete m_RxBuf[i];
m_RxBuf[i] = NULL;
m_RxLen[i] = 0;
}
}
if (_fileBuf != NULL)
delete _fileBuf;
_fileBuf = NULL;
fclose(m_pFile);
m_pFile = NULL;
QMessageBox::warning(NULL, QObject::tr("warning"), QObject::tr("Memory request failed"));
return false;
}
setvbuf(m_pFile, _fileBuf, _IOFBF, BLOCKSIZE);
for(int i = BlockNum; i < BUFBLOCKNUM_MAX; i++)
{
m_RxBuf[i] = NULL;
m_RxLen[i] = 0;
}
m_iBufBlockSize = BlockNum;
m_iWr_Block_Ptr = 0;
m_iWr_Ptr = 0;
m_iRd_Block_Ptr = m_iBufBlockSize - 1;
m_bStart = true;
return true;
}
bool FileSave::Stop()
{
if(!m_bStart) return false;
m_bStart = false;
m_mFileLock.lock();
fclose(m_pFile);
m_pFile = NULL;
delete _fileBuf;
_fileBuf = NULL;
m_mFileLock.unlock();
//释放原有缓存空间
for(int i = 0; i < BUFBLOCKNUM_MAX; i++)
{
if(m_RxBuf[i] != NULL)
{
delete []m_RxBuf[i];
}
m_RxBuf[i] = NULL;
m_RxLen[i] = 0;
}
m_iBufBlockSize = 0;
return true;
}
void FileSave::pushPacket(unsigned char* pPkt,unsigned int iLen)
{
if(!m_bStart) return;
m_mBufLock.lock();
if((m_iWr_Ptr+iLen) > BLOCKSIZE)
{
m_RxLen[m_iWr_Block_Ptr] = m_iWr_Ptr;
ReleaseSemaphore(m_hDataReady[0], 1, NULL);
m_iWr_Ptr = 0;
m_iWr_Block_Ptr = (m_iWr_Block_Ptr + 1) % m_iBufBlockSize;
//m_mFileLock.Lock();
if(m_iWr_Block_Ptr == m_iRd_Block_Ptr)
{
//TRACE("缓冲区溢出:Wr=%d,Rd=%d\n",m_iWr_Block_Ptr,m_iRd_Block_Ptr);
//qDebug("缓冲区溢出:Wr=%d,Rd=%d\n",m_iWr_Block_Ptr,m_iRd_Block_Ptr);
}
//m_mFileLock.Unlock();
}
memcpy(m_RxBuf[m_iWr_Block_Ptr] + m_iWr_Ptr, pPkt, iLen);
m_iWr_Ptr += iLen;
m_mBufLock.unlock();
}
void FileSave::SaveData()
{
while(true)
{
//等待事件
DWORD dwEvent = WaitForMultipleObjects(2, m_hDataReady, FALSE, 1000);
if (dwEvent == WAIT_OBJECT_0 + 0) // 数据到达
{
//保存数据到文件
if(m_bStart)
{
// 获取读地址与数据长度
m_mBufLock.lock();
m_iRd_Block_Ptr = (m_iRd_Block_Ptr + 1) % m_iBufBlockSize;
//存盘
//m_mFileLock.lock();
fwrite(m_RxBuf[m_iRd_Block_Ptr], m_RxLen[m_iRd_Block_Ptr], 1, m_pFile);
//fflush(m_pFile);
//m_mFileLock.unlock();
m_mBufLock.unlock();
}
}
else if(dwEvent == WAIT_OBJECT_0 + 1) // 线程退出
{
break;
}
else if (dwEvent == WAIT_TIMEOUT) // 超时
{
if(m_bStart)
{
m_mBufLock.lock();
if(m_iWr_Ptr != 0)
{
m_RxLen[m_iWr_Block_Ptr] = m_iWr_Ptr;
ReleaseSemaphore(m_hDataReady[0], 1, NULL);
m_iWr_Ptr = 0;
m_iWr_Block_Ptr = (m_iWr_Block_Ptr + 1) % m_iBufBlockSize;
}
m_mBufLock.unlock();
}
}
}
}
代码实现看起来,较麻烦。但是,使用起来,倒是十分简单,主要就是如下3个函数:
// 开始记录
bool Start(const char* FileName, unsigned int BlockNum = BUFBLOCKNUM_DEF);
// 停止记录
bool Stop();
// 接收数据函数
void pushPacket(unsigned char* pPkt,unsigned int iLen);
具体用法,看如下的测试代码,mainwindow.cpp。
三、测试代码
mainwindow.cpp
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->pushButton_start->setEnabled(true);
ui->pushButton_stop->setEnabled(false);
// 生成256KB测试数据,因为每次推送数据不能大于BLOCKSIZE=512KB
createTestData(data, 256 * 1024);
fileSave = new FileSave();
// 每隔1s推送256KB数据,用于模拟真实接收数据环境
startTimer(1000);
}
MainWindow::~MainWindow()
{
delete fileSave;
fileSave = nullptr;
delete ui;
}
void MainWindow::timerEvent(QTimerEvent *event)
{
fileSave->pushPacket((unsigned char*)data.data(), data.length());
}
void MainWindow::createTestData(QByteArray& data, int size)
{
data.resize(size);
for (int i = 0; i < size; i++)
{
data[i] = i % 128;
}
}
void MainWindow::on_pushButton_start_clicked()
{
QString fileName = QUuid::createUuid().toString();
QString filePath = qApp->applicationDirPath() + "/" + fileName + ".data";
fileSave->Start(filePath.toStdString().c_str());
ui->pushButton_start->setEnabled(false);
ui->pushButton_stop->setEnabled(true);
}
void MainWindow::on_pushButton_stop_clicked()
{
fileSave->Stop();
ui->pushButton_start->setEnabled(true);
ui->pushButton_stop->setEnabled(false);
}
先生成256KB测试数据,并通过定时器每隔1s,将该数据进行一次推送,用于模拟真实接收到数据的情景。
在定时器响应函数中,通过FileSave的pushPacket接口,将数据推送到内部的环形缓冲区。
-
当点击“开始存储”按钮时,创建文件,并在独立的线程中,自动从环形缓冲区中,不断取出数据,并写入文件。
-
当点击“停止存储”按钮时,停止取数据,并关闭文件。
当没有开始存储时,线程不会从缓冲区中取数据,缓冲区中的数据,会自动被循环覆盖(新的数据块自动覆盖最久远的数据块)。
该类存储的行为,类似于从流动的河水中,从某个位置,持续取出一段水流。
运行效果:
本文涉及工程代码:
https://gitee.com/bailiyang/cdemo/tree/master/C++/23FileSaveTest/FileSaveTest
若对你有帮助,欢迎点赞、收藏、评论,你的支持就是我的最大动力!!!
同时,阿超为大家准备了丰富的学习资料,欢迎关注公众号“超哥学编程”,即可领取。