环形缓冲区(Cycle Buffer)的 C++ 实现与实际项目应用

在嵌入式系统和实时数据处理中,环形缓冲区(也称为循环缓冲区)是一种高效的数据存储结构,特别适用于生产者 - 消费者模型。最近在项目中正好用到了,实现了一个支持多线程安全的版本,今天就来聊聊这个实现的细节和实际使用中的一些心得。

环形缓冲区简介

做过串口通信或者网络数据处理的同学应该都有体会,数据往往是断断续续到来的,而且读写速度可能不匹配。环形缓冲区的优势就在于:

  • 固定内存空间,避免频繁动态分配的开销
  • 首尾相连的结构,天然适合 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;
    }
}

帧解析功能

该实现特别增加了帧解析功能,适合处理结构化的数据包:

  1. 帧头查找ResultBitIdx方法在缓冲区中查找帧头
  2. 帧长度计算getFrameLen根据帧信息计算帧长度
  3. 帧尾校验checkTailCorrect验证帧尾正确性
  4. 帧提取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,独占访问权
  • 自动加锁和解锁,避免手动管理锁带来的风险

使用场景

  1. 串口数据处理:接收不定时的串口数据,进行帧解析
  2. 网络数据缓存:缓存网络接收的数据包,按需读取
  3. 音频 / 视频流处理:实时处理媒体流数据
  4. 传感器数据采集:缓存传感器输出的连续数据

使用中的一些小技巧

  1. 缓冲区大小选择:根据实际数据量来定,太大浪费内存,太小容易溢出。我们项目里用了 20480 字节,刚好满足需求。
  2. 帧信息配置:通过setFrameInfo设置帧头、帧尾、长度字段位置等,灵活适应不同协议。
  3. 错误处理:加了详细的日志,方便调试时定位问题。
  4. 边界处理:统一用模运算处理环形跳转,避免逻辑混乱。

总结

这个环形缓冲区在项目中用了挺久,稳定性还不错。主要优势是:

  • 代码简洁,逻辑清晰
  • 线程安全,适合多线程环境
  • 支持帧解析,对协议数据处理很友好
  • 内存管理高效,没有频繁分配释放

当然也有可以优化的地方,比如可以加个回调函数,当缓冲区达到一定阈值时通知外部处理。如果大家有更好的想法,欢迎交流~

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值