在嵌入式系统和实时数据处理中,环形缓冲区(也称为循环缓冲区)是一种高效的数据存储结构,特别适用于生产者 - 消费者模型。最近在项目中正好用到了,实现了一个支持多线程安全的版本,今天就来聊聊这个实现的细节和实际使用中的一些心得。
环形缓冲区简介
做过串口通信或者网络数据处理的同学应该都有体会,数据往往是断断续续到来的,而且读写速度可能不匹配。环形缓冲区的优势就在于:
- 固定内存空间,避免频繁动态分配的开销
- 首尾相连的结构,天然适合 FIFO(先进先出)的数据处理
- 用两个指针就能管理,实现简单高效
- 特别适合生产者 - 消费者模型
我们这个实现主要用于处理传感器数据和图像帧,需要兼顾效率和线程安全,所以加了读写锁保护。
核心实现解析
我们的环形缓冲区实现包含两个文件:cyclebuffer.h和cyclebuffer.cpp,主要类为CCycleBuffer。
class CCycleBuffer
{
public:
CCycleBuffer(quint32 size);
~CCycleBuffer();
// 核心功能函数
bool isFull();
bool isEmpty();
void Clear();
quint32 GetLength();
quint32 GetFreeLength();
quint32 push(quint8* buf, quint32 count);
quint32 pop(quint8* buf, quint32 count);
private:
// 辅助函数
bool ResultBitIdx(int& rtBitidx);
int getFrameLen(int nBytePos);
bool checkTailCorrect(quint8* buf, int rtBitidx, int FramLen);
quint32 distance(quint32 from, quint32 to) const;
// 成员变量
bool m_bEmpty; // 缓冲区为空标志
bool m_bFull; // 缓冲区已满标志
char* m_pBuf; // 缓冲区数据指针
quint32 m_nBufSize; // 缓冲区大小
quint32 m_nPopPos; // 读指针位置
quint32 m_nPushPos; // 写指针位置
QReadWriteLock* m_lock; // 读写锁,用于多线程安全
ST_FRAME_INFO m_frameInfo; // 帧信息,用于帧解析
};
初始化与销毁
构造函数需要指定缓冲区大小,这里有个小细节,刚开始写的时候用了sizeof(m_pBuf)来初始化内存,结果发现错了(指针大小和缓冲区大小搞混了),后来改成直接用传入的 size:
CCycleBuffer::CCycleBuffer(quint32 size)
: m_nBufSize(size)
, m_nPopPos(0)
, m_nPushPos(0)
, m_bEmpty(true)
, m_bFull(false)
, m_lock(new QReadWriteLock) {
m_pBuf = new char[m_nBufSize];
std::memset(m_pBuf, 0, m_nBufSize);
}
CCycleBuffer::~CCycleBuffer() {
delete[] m_pBuf;
m_frameInfo.clear();
delete m_lock;
}
核心操作实现
数据写入(push)
push 函数是核心之一,需要处理两种情况:缓冲区未满和已满。当缓冲区满时,我们选择重置状态后重试(实际项目中根据需求也可以选择覆盖或丢弃)。
写入时最麻烦的是环形边界处理,比如写指针快到末尾时,需要分两段写入:
quint32 CCycleBuffer::push(quint8* buf, quint32 count) {
if (count == 0 || buf == nullptr) {
LOG(WARNING) << "push failed: invalid input (count=0 or buf=null)";
return 0;
}
QWriteLocker locker(m_lock); // 自动管理写锁
m_bEmpty = false;
if (m_bFull) {
LOG(INFO) << "Cache is full, resetting";
m_bFull = false;
return push(buf, count);
}
// 计算可写入长度
const quint32 freeLen = GetFreeLengthUnlock();
const quint32 writeLen = std::min(count, freeLen);
if (writeLen == 0) return 0;
// 分两段写入(处理环形边界)
const quint32 firstSeg = std::min(writeLen, m_nBufSize - m_nPushPos);
std::memcpy(m_pBuf + m_nPushPos, buf, firstSeg);
if (writeLen > firstSeg) {
const quint32 secondSeg = writeLen - firstSeg;
std::memcpy(m_pBuf, buf + firstSeg, secondSeg);
}
// 更新写入位置
m_nPushPos = (m_nPushPos + writeLen) % m_nBufSize;
m_bFull = (m_nPushPos == m_nPopPos); // 满状态判断
return writeLen;
}
数据读取(pop)
pop 函数和 push 类似,也要处理环形边界,逻辑是对称的:
quint32 CCycleBuffer::pop(quint8* buf, quint32 count) {
if (count == 0 || buf == nullptr || m_bEmpty) {
return 0;
}
QReadLocker locker(m_lock); // 自动管理读锁
m_bFull = false;
// 计算可读取长度
const quint32 dataLen = GetLengthUnlock();
const quint32 readLen = std::min(count, dataLen);
if (readLen == 0) return 0;
// 分两段读取
const quint32 firstSeg = std::min(readLen, m_nBufSize - m_nPopPos);
std::memcpy(buf, m_pBuf + m_nPopPos, firstSeg);
if (readLen > firstSeg) {
const quint32 secondSeg = readLen - firstSeg;
std::memcpy(buf + firstSeg, m_pBuf, secondSeg);
}
// 更新读取位置
m_nPopPos = (m_nPopPos + readLen) % m_nBufSize;
m_bEmpty = (m_nPopPos == m_nPushPos); // 空状态判断
return readLen;
}
长度计算
环形缓冲区的长度计算是个关键点,需要区分读指针在前还是在后:
quint32 CCycleBuffer::GetLengthUnlock() {
if (m_nPopPos == m_nPushPos)
{
m_bFull = false;
m_bEmpty = true;
}
if (m_bEmpty)
{
return 0;
}
if (m_nPopPos == m_nPushPos)
{
return m_nBufSize;
}
else if (m_nPopPos < m_nPushPos) // 读指针在写指针前
{
return m_nPushPos - m_nPopPos;
}
else // 读指针在写指针后(环形绕回)
{
return m_nBufSize - m_nPopPos + m_nPushPos;
}
}
帧解析功能
该实现特别增加了帧解析功能,适合处理结构化的数据包:
- 帧头查找:
ResultBitIdx方法在缓冲区中查找帧头 - 帧长度计算:
getFrameLen根据帧信息计算帧长度 - 帧尾校验:
checkTailCorrect验证帧尾正确性 - 帧提取:
popFrame方法提取完整帧
bool CCycleBuffer::popFrame(quint8* buf, int& len) {
len = 0;
if (buf == nullptr || m_bEmpty) {
return false;
}
QReadLocker readLocker(m_lock);
int frameHeadPos = 0;
if (!ResultBitIdx(frameHeadPos)) {
// 未找到帧头,移动读指针
if (!m_bEmpty) {
m_nPopPos = (m_nPopPos + 1) % m_nBufSize;
m_bEmpty = (m_nPopPos == m_nPushPos);
}
return false;
}
// 后续帧长度检查和数据拷贝...
}
多线程安全
实现中使用QReadWriteLock保证多线程安全:
- 读操作使用
QReadLocker,允许多个读者同时访问 - 写操作使用
QWriteLocker,独占访问权 - 自动加锁和解锁,避免手动管理锁带来的风险
使用场景
- 串口数据处理:接收不定时的串口数据,进行帧解析
- 网络数据缓存:缓存网络接收的数据包,按需读取
- 音频 / 视频流处理:实时处理媒体流数据
- 传感器数据采集:缓存传感器输出的连续数据
使用中的一些小技巧
- 缓冲区大小选择:根据实际数据量来定,太大浪费内存,太小容易溢出。我们项目里用了 20480 字节,刚好满足需求。
- 帧信息配置:通过
setFrameInfo设置帧头、帧尾、长度字段位置等,灵活适应不同协议。 - 错误处理:加了详细的日志,方便调试时定位问题。
- 边界处理:统一用模运算处理环形跳转,避免逻辑混乱。
总结
这个环形缓冲区在项目中用了挺久,稳定性还不错。主要优势是:
- 代码简洁,逻辑清晰
- 线程安全,适合多线程环境
- 支持帧解析,对协议数据处理很友好
- 内存管理高效,没有频繁分配释放
当然也有可以优化的地方,比如可以加个回调函数,当缓冲区达到一定阈值时通知外部处理。如果大家有更好的想法,欢迎交流~
1006

被折叠的 条评论
为什么被折叠?



