本帖最后由 hubeiluhua 于 2025-7-8 10:47 编辑
环形缓冲区 vs 环形队列的区别?为什么环形缓冲区不需要加锁? 在嵌入式开发中,特别是裸机、中断驱动的场景下,经常会使用“环形缓冲区”或“环形队列”来实现数据缓存,比如串口接收缓冲、DMA缓存等。 很多人会问: 这篇笔记将解释这两个概念的本质区别,并深入分析环形缓冲区为什么可以实现“无锁通信”。 一、基本定义 二、最小实现示例 【环形缓冲区示例】: #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);
}
}
这个例子中: 【环形队列封装示例】: 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; 互不访问对方的变量,不存在数据竞争。
指针访问是原子的
判断条件只有主循环在用
只要不越界、不混用指针,整个结构在中断与主循环之间是天然线程安全的。 四、什么时候需要加锁? 以下情况必须使用锁、关中断或原子操作: 多个中断同时写入环形缓冲区(多个生产者); 中断中读取 read_idx,主循环也读取; 队列结构中修改了共享计数器 count,且被多个上下文访问; 使用结构体、uint64_t 等非原子类型作为索引指针; 多线程/多任务环境(如 RTOS 中多个任务访问同一个队列)。
五、总结对比 环形缓冲区: 面向底层,高性能; 主循环读,ISR 写; 只用读写指针,无需加锁; 不判断满,有覆盖风险(可扩展); 非常适合串口接收、DMA数据接收等。
环形队列: 面向抽象,功能完整; 支持队满队空判断; 修改多个共享变量,通常需要保护; 适合任务间通信、双向传输等复杂场景。
六、实际工程建议 如果只是单向通信:ISR 写入,主循环读取,推荐用环形缓冲区,无锁高效; 如果需要判断是否满/空、支持多任务访问,推荐封装成环形队列并加锁; 不要在 ISR 和主循环同时访问相同的变量(如 read_idx); 所有索引指针尽量使用 MCU 支持的原子访问类型(如 uint8_t、uint16_t)。
以上就是环形缓冲区和环形队列的区别说明,以及为什么环形缓冲区可以不加锁的原理分析。 欢迎补充与交流。
|