环形缓冲区与环形队列的区别
本帖最后由 hubeiluhua 于 2025-7-8 10:47 编辑环形缓冲区 vs 环形队列的区别?为什么环形缓冲区不需要加锁?在嵌入式开发中,特别是裸机、中断驱动的场景下,经常会使用“环形缓冲区”或“环形队列”来实现数据缓存,比如串口接收缓冲、DMA缓存等。很多人会问:
[*]“环形缓冲区”和“环形队列”有什么区别?
[*]为什么环形缓冲区在很多情况下不需要加锁,也不会数据错乱?
这篇笔记将解释这两个概念的本质区别,并深入分析环形缓冲区为什么可以实现“无锁通信”。一、基本定义
[*]环形缓冲区(circular buffer):
本质是一块连续数组,通过两个指针:写指针 write_idx 和读指针 read_idx 实现循环读写。
通常用于中断写、主循环读的高速通信。
[*]环形队列(circular queue):
是在环形缓冲区基础上封装的“队列”数据结构,具备入队、出队、队满、队空等操作,适合任务通信或需要状态管理的场景。
二、最小实现示例【环形缓冲区示例】:#define BUF_SIZE 64
volatile uint8_t buf;
volatile uint8_t write_idx = 0;
volatile uint8_t read_idx = 0;
void USART_IRQHandler(void)
{
buf = 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 + 1) % BUF_SIZE;
process(data);
}
}
这个例子中:
[*]USART中断不断写入数据;
[*]主循环不断读取数据;
[*]中断只修改 write_idx,主循环只修改 read_idx;
[*]因此读写互不干扰,不需要加锁。
【环形队列封装示例】:typedef struct {
uint8_t buf;
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 = 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 + 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)。
以上就是环形缓冲区和环形队列的区别说明,以及为什么环形缓冲区可以不加锁的原理分析。欢迎补充与交流。
页:
[1]