打印

环形缓冲区与环形队列的区别

[复制链接]
55|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
hubeiluhua|  楼主 | 2025-7-8 10:33 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 hubeiluhua 于 2025-7-8 10:47 编辑

环形缓冲区 vs 环形队列的区别?为什么环形缓冲区不需要加锁?
在嵌入式开发中,特别是裸机、中断驱动的场景下,经常会使用“环形缓冲区”或“环形队列”来实现数据缓存,比如串口接收缓冲、DMA缓存等。
很多人会问:
  • “环形缓冲区”和“环形队列”有什么区别?
  • 为什么环形缓冲区在很多情况下不需要加锁,也不会数据错乱?

这篇笔记将解释这两个概念的本质区别,并深入分析环形缓冲区为什么可以实现“无锁通信”。
一、基本定义
  • 环形缓冲区(circular buffer):
    本质是一块连续数组,通过两个指针:写指针 write_idx 和读指针 read_idx 实现循环读写。
    通常用于中断写、主循环读的高速通信。
  • 环形队列(circular queue):
    是在环形缓冲区基础上封装的“队列”数据结构,具备入队、出队、队满、队空等操作,适合任务通信或需要状态管理的场景。

二、最小实现示例
【环形缓冲区示例】:
#define BUF_SIZE 64
volatile uint8_t buf[BUF_SIZE];
volatile uint8_t write_idx = 0;
volatile uint8_t read_idx = 0;

void USART_IRQHandler(void)
{
    buf[write_idx] = USART_ReceiveData();
    write_idx = (write_idx + 1) % BUF_SIZE;
}

void main_loop(void)
{
    while (read_idx != write_idx) {
        uint8_t data = buf[read_idx];
        read_idx = (read_idx + 1) % BUF_SIZE;
        process(data);
    }
}

这个例子中:
  • USART中断不断写入数据;
  • 主循环不断读取数据;
  • 中断只修改 write_idx,主循环只修改 read_idx;
  • 因此读写互不干扰,不需要加锁。

【环形队列封装示例】:
typedef struct {
    uint8_t buf[64];
    uint8_t head;
    uint8_t tail;
    uint8_t count;
} RingQueue;

bool enqueue(RingQueue *q, uint8_t val)
{
    if (q->count == sizeof(q->buf)) return false;
    q->buf[q->tail] = val;
    q->tail = (q->tail + 1) % sizeof(q->buf);
    q->count++;
    return true;
}

bool dequeue(RingQueue *q, uint8_t *out)
{
    if (q->count == 0) return false;
    *out = q->buf[q->head];
    q->head = (q->head + 1) % sizeof(q->buf);
    q->count--;
    return true;
}

这个环形队列版本更完整,有队满、队空判断,但因为修改了多个共享变量(count、head、tail),所以通常需要加锁或关中断保护。
三、为什么环形缓冲区可以不用加锁?
  • 读写操作天然分离

    • ISR 只负责写数据并推进 write_idx;
    • 主循环只负责读数据并推进 read_idx;
    • 互不访问对方的变量,不存在数据竞争。

  • 指针访问是原子的

    • MCU 对 uint8_t、uint16_t、uint32_t 类型变量的读写操作是原子的;
    • 所以 ISR 和主循环“同时”更新各自的指针是安全的。

  • 判断条件只有主循环在用

    • 主循环使用 read_idx != write_idx 判断缓冲区非空;
    • ISR 不需要知道 read_idx 的值,因此不影响判断准确性。


只要不越界、不混用指针,整个结构在中断与主循环之间是天然线程安全的。
四、什么时候需要加锁?
以下情况必须使用锁、关中断或原子操作:
  • 多个中断同时写入环形缓冲区(多个生产者);
  • 中断中读取 read_idx,主循环也读取;
  • 队列结构中修改了共享计数器 count,且被多个上下文访问;
  • 使用结构体、uint64_t 等非原子类型作为索引指针;
  • 多线程/多任务环境(如 RTOS 中多个任务访问同一个队列)。

五、总结对比
环形缓冲区:
  • 面向底层,高性能;
  • 主循环读,ISR 写;
  • 只用读写指针,无需加锁;
  • 不判断满,有覆盖风险(可扩展);
  • 非常适合串口接收、DMA数据接收等。

环形队列:
  • 面向抽象,功能完整;
  • 支持队满队空判断;
  • 修改多个共享变量,通常需要保护;
  • 适合任务间通信、双向传输等复杂场景。

六、实际工程建议
  • 如果只是单向通信:ISR 写入,主循环读取,推荐用环形缓冲区,无锁高效;
  • 如果需要判断是否满/空、支持多任务访问,推荐封装成环形队列并加锁;
  • 不要在 ISR 和主循环同时访问相同的变量(如 read_idx);
  • 所有索引指针尽量使用 MCU 支持的原子访问类型(如 uint8_t、uint16_t)。

以上就是环形缓冲区和环形队列的区别说明,以及为什么环形缓冲区可以不加锁的原理分析。
欢迎补充与交流。

使用特权

评论回复

相关帖子

发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

11

主题

29

帖子

1

粉丝