C语言无锁高并发安全环形缓冲队列设计(一)

1、前言

队列,常用数据结构之一,特点是先进先出。

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。


2、设计思想

从理想的无限缓冲区到实际的使用进行设计思考。

2.1、理想化的无限缓冲区

在理想情况下,写入器(数据生产者)和读取器(数据消费者)在使用过程中,能够访问相同的、连续的、并且是无限的缓冲区。写入器和读取器各自保存一个索引,分别指向缓冲区中写和读的位置,即与之对齐的“写”和“读”指针开始进行操作。

当写入器想要在末端加入数据时,它会将数据追加到“写索引”后面的位置,之后将“写索引”移动到新写数据块的末尾。而读取器在顶端读取数据时,如果“写索引”比“读索引”位置大时,读写器就可以对已有数据进行读取操作。完成之后,读写器将“读索引”移动到读取数据块的末尾,以跟踪记录已经处理至缓冲区的哪一部分。

读取器永远不会试图读取超过“写索引”位置的数据,因为不能保证那里有有效的数据,即写入器在那里写入了东西。这也意味着“读索引”永远不能超过“写索引”。目前为止,我们假设这个理想内存系统总是连贯一致的,也就是说一旦执行了写操作,数据可以立即地、顺序地进行读取出来。


2.2、有界限的缓冲区

而现实中计算机并没有神奇的无限缓冲区。我们必须分配有限的内存空间,以供写入器和读取器之间进行或许无限的使用。在循环缓冲区中,“写索引”可以在抵达缓冲区末尾时跨越缓冲区的边界回绕到缓冲区的开始位置。

当“写索引”接近缓冲区末尾并且又有新数据输入进来时,会将写操作分成两个数据块:一个写入缓冲区末尾剩余的空间,另一个将剩下的数据写入缓冲区开头。请注意,如果此时“读索引”位置接近缓冲区开头,那么这个写入操作可能会破坏尚未被读取器处理的数据。由于这个原因,“写索引”在被回绕后不允许超过“读索引”

这样下来,可能会得到两种内存分布情况:

  1. “写索引”在前面,“读索引”在后面,即在索引移动方向上,“写索引”超前于“读索引”,已写入但未被读取器处理的有效数据位于“读索引”和“写索引”之间的缓冲区内;

  2. “读索引”在前面,“写索引”在后面,即在索引移动方向上,“读索引”超前于“写索引”,有效数据开始于“读索引”,结束于缓冲区末尾,并回绕到缓冲区开头直至“写索引”位置。

 注意:上述第二种情况下,“写索引”和“读索引”可能存在重合,为了区分这两种情况,一般规定第二种情况下“写索引”和“读索引”不允许重合,即“写索引”位置必须落后于“读索引”一步。

因此,在循环缓冲区中,不断地从内存分布情况 1 进行到内存分布情况 2,又从 2 再回到 1,如此循环往复,当“读索引”到达缓冲区的末尾时,也回绕到缓冲区开头继续进行读取。


2.3、并发性设计

通常在使用缓冲区队列时,读数据和写数据大部分情况都是多线程或者前后台(中断)分别处理的,为了减少写入器、读取器两个线程之间或者前后台系统之间需要发生的协调,一种常见策略是,将读写变量独立出来,分别由读取器和写入器进行改变。这也简化了并发性设计中的逻辑推理,因为谁负责更改哪个变量总是很清楚。

让我们从一个简单的循环缓冲区开始,它具有原始的“写索引”和“读索引”。唯有写入器能更改“写索引”,而唯有读取器能更改“读索引”

这样就可以不用锁进行操作,提高效率。


2.4、如何保证地址的连续性

在上述提到的有界缓冲区内存分布情况,第二种情况无法保证地址的连续性,因为有些场景需要使用到连续的内存块地址,解决这种场景的办法有:可以对缓存区进行分块,每一块固定的长度,即固定长度的队列,这样就能在一定程度上保证了地址的连续性。


3、代码实现

3.1、队列结构体定义

先定义一个队列结构体,包含了每个块的大小、数目、写入块索引、读取块索引等,为了解决“写索引”和“读索引”可能存在重合的两种情况,加入状态变量用来区分。

typedef uint16_t queuesize_t;

typedef struct{
    volatile uint8_t state; /*!< 控制状态 */

    queuesize_t end;        /*!< 循环队列尾哨兵 */

    queuesize_t head;       /*!< 循环队列首哨兵 */

    queuesize_t num;        /*!< 循环队列中能存储的最多组数 */
    
    queuesize_t size;       /*!< 单组内存大小 */

    char  *pBufMem;         /*!< Buf 指针 */
} QueueCtrl_t;

3.2、队列初始化

定义结构体后一定要对数据初始化,同时为接口提供一些入参变量设置队列的信息进行封装,如缓存区地址、队列的组数目、组内存大小和是否内存覆盖等信息。

/**
  * @brief      队列控制初始化, 可采用的是动态/静态内存分配方式
  *
  * @param[in,out] pCtrl 队列控制句柄
  * @param[in]  buf      buf 地址
  * @param[in]  queueNum 队列数目大小
  * @param[in]  size     队列中单个内存大小
  * @param[in]  isCover  false,不覆盖; true,队列满了覆盖未读取的最早旧数据
  * @return     0,成功; -1,失败
  */
int Queue_Init(QueueCtrl_t *pCtrl, const void *pBufMem, queuesize_t queueNum, queuesize_t size, bool isCover)
{
    if (pCtrl == NULL || pBufMem == NULL || queueNum == 0 || size == 0)
    {
        return -1;
    }

    pCtrl->end     = 0;
    pCtrl->head    = 0;
    pCtrl->pBufMem = (char *)pBufMem;
    pCtrl->num  = queueNum;
    pCtrl->size    = size;
    pCtrl->state   = 0x00;
    
    if (isCover)
    {
        QUEUE_STATE_SET(pCtrl, QUEUE_ENABLE_COVER);
    }

    return 0;
}

3.3、队列写操作

队列都是在末端增加数据,因为队列组的大小已经固定,因此在写操作数据时可以省略新数据的内存大小。

/**
  * @brief      在队列末尾加入新的数据
  *
  * @param[in,out] pCtrl 队列控制句柄
  * @param[in]     src   新的数据
  * @retval     返回的值含义如下
  *             @arg 0: 写入成功
  *             @arg -1: 写入失败
  */
extern int Queue_Push(QueueCtrl_t *pCtrl, const void *src)
{
    if (pCtrl == NULL || src == NULL)
    {
        return -1;
    }
    
    if (IS_QUEUE_STATE_SET(pCtrl, QUEUE_DISABLE_PUSH))
    {
        return -1;
    }
    
    memcpy(&pCtrl->pBufMem[pCtrl->end * pCtrl->size], src, pCtrl->size);
    pCtrl->end++;
    QUEUE_STATE_SET(pCtrl, QUEUE_EXIT_DATA);

    if ((pCtrl->end) >= (pCtrl->num))
    {
        (pCtrl->end) = 0;
    }

    if (IS_QUEUE_STATE_SET(pCtrl, QUEUE_DATA_FULL))
    {
        (pCtrl->head) = (pCtrl->end);
    }
    else if ((pCtrl->end) == (pCtrl->head))
    {
        QUEUE_STATE_SET(pCtrl, QUEUE_DATA_FULL);

        if (!IS_QUEUE_STATE_SET(pCtrl, QUEUE_ENABLE_COVER))
        {
            QUEUE_STATE_SET(pCtrl, QUEUE_DISABLE_PUSH);
        }
    }

    return 0;
}

3.4、队列读操作

队列都是在顶端读取数据,因为队列组的大小已经固定,因此在都操作数据时可以省略数据读取存入的内存大小(必须保证读取的内存大小足够)。

/**
  * @brief      读取并弹出队列顶端的数据
  *
  * @param[in,out] pCtrl 队列控制句柄
  * @param[in]     dest  读取的数据
  * @retval     返回的值含义如下
  *             @arg 0: 成功
  *             @arg -1: 失败
  */
int Queue_Pop(QueueCtrl_t *pCtrl, void *dest)
{
    if (pCtrl == NULL)
    {
        return -1;
    }

    if (!IS_QUEUE_STATE_SET(pCtrl, QUEUE_EXIT_DATA))
    {
        return -1;
    }

    memcpy((char *)dest, &pCtrl->pBufMem[pCtrl->head * pCtrl->size], pCtrl->size);
    pCtrl->head++;

    if ((pCtrl->head) >= (pCtrl->num))
    {
        pCtrl->head = 0;
    }

    if ((pCtrl->head) == (pCtrl->end))
    {
        if (!IS_QUEUE_STATE_SET(pCtrl, QUEUE_DATA_FULL))
        {
            QUEUE_STATE_CLEAR(pCtrl, QUEUE_EXIT_DATA);
        }
    }

    QUEUE_STATE_CLEAR(pCtrl, QUEUE_DISABLE_PUSH);
    QUEUE_STATE_CLEAR(pCtrl, QUEUE_DATA_FULL);

    return 0;
}

4、代码实现

代码可下载:queue

  • 4
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
### 回答1: 我可以给你一个简单的示例程序: #include <stdio.h> #include <string.h> #include <stdlib.h>#define BUFFER_SIZE 8typedef struct { unsigned char buffer[BUFFER_SIZE]; int head; int tail; int count; } RingBuffer;// 初始化环形队列 void init_ring_buffer(RingBuffer *rb) { memset(rb->buffer, 0, BUFFER_SIZE); rb->head = 0; rb->tail = 0; rb->count = 0; }// 向环形队列放入数据 int put_data(RingBuffer *rb, unsigned char data) { if (rb->count == BUFFER_SIZE) { return -1; } rb->buffer[rb->tail] = data; rb->tail = (rb->tail + 1) % BUFFER_SIZE; rb->count++; return 0; }// 从环形队列取出数据 int get_data(RingBuffer *rb, unsigned char *data) { if (rb->count == 0) { return -1; } *data = rb->buffer[rb->head]; rb->head = (rb->head + 1) % BUFFER_SIZE; rb->count--; return 0; }int main(void) { // 使用环形队列缓冲串口接收的数据 RingBuffer rb; init_ring_buffer(&rb); // 串口接收到的数据 unsigned char data; // 将接收到的数据放入环形队列 put_data(&rb, data); // 从环形队列中取出数据 get_data(&rb, &data); return 0; } ### 回答2: 串口接收是通过串口接口接收来自外部设备发送的数据。而环形队列缓冲是一种数据结构,用于在接收数据时存储和处理数据。 在C语言中,可以使用串口的库函数来实现串口通信,并结合环形队列缓冲实现数据的接收。下面是一个使用C语言实现串口接收并使用环形队列缓冲的示例代码: ```c #include <stdio.h> #define MAX_BUFFER_SIZE 100 typedef struct { char data[MAX_BUFFER_SIZE]; int front, rear, count; } CircularBuffer; void initBuffer(CircularBuffer *buffer) { buffer->front = buffer->rear = buffer->count = 0; } void enqueue(CircularBuffer *buffer, char ch) { if (buffer->count < MAX_BUFFER_SIZE) { buffer->data[buffer->rear] = ch; buffer->rear = (buffer->rear + 1) % MAX_BUFFER_SIZE; buffer->count++; } else { printf("Buffer is full!\n"); } } char dequeue(CircularBuffer *buffer) { char ch; if (buffer->count > 0) { ch = buffer->data[buffer->front]; buffer->front = (buffer->front + 1) % MAX_BUFFER_SIZE; buffer->count--; return ch; } else { printf("Buffer is empty!\n"); return 0; } } void receiveData(CircularBuffer *buffer, char *data, int size) { for (int i = 0; i < size; i++) { enqueue(buffer, data[i]); } } int main() { CircularBuffer buffer; initBuffer(&buffer); char receivedData[] = "Hello, World!"; receiveData(&buffer, receivedData, sizeof(receivedData) - 1); // sizeof(receivedData) - 1是为了去除字符串末尾的'\0'字符 // 从缓冲区中逐个取出字符并打印 while (buffer.count > 0) { char ch = dequeue(&buffer); printf("%c", ch); } printf("\n"); return 0; } ``` 以上代码使用了一个`CircularBuffer`结构体来表示环形队列缓冲区,其中`data`是用于存储接收到的字符的数组,`front`和`rear`分别表示环形队列的前端和后端,`count`表示缓冲区中元素的数量。通过`enqueue`函数将接收到的字符放入缓冲区,通过`dequeue`函数将缓冲区中的字符取出。`receiveData`函数在串口接收到数据时调用,将数据存储到缓冲区中。在`main`函数中,首先初始化缓冲区,然后模拟接收到数据并存入缓冲区,最后逐个取出字符并打印出来。 这样就完成了一个用C语言实现串口接收并使用环形队列缓冲的示例代码。通过环形队列缓冲,可以高效地存储和处理串口接收到的数据。 ### 回答3: 使用C语言编写串口接收程序,可以使用环形队列缓冲来存储接收到的数据。 首先,需要定义一个环形队列的结构体,包含队列的大小、当前队列的长度、队头和队尾等信息。在实际编码中,可以用数组来表示环形队列,通过定义一个读指针和写指针指向队列的头尾,对队列进行读写操作。 接着,在程序中打开串口,并设置串口参数,如波特率、数据位、停止位和校验位等。可以使用C语言提供的串口库函数来简化这一步骤,如`open()`、`ioctl()`和`read()`函数等。 然后,需要定义一个缓冲区用来存储从串口接收到的数据。缓冲区可以是一个全局变量,定义为一个字符数组,并初始化为空。同时定义一个变量记录缓冲区中数据的长度。 接下来,在串口接收中断处理函数中,读取从串口接收到的数据,并将数据存入环形队列缓冲中。此时,需要判断队列是否已满,若满了,则丢弃最早的数据,即将队头指针后移一个位置,然后将新数据写入队尾,并更新队尾指针。 在主函数中,可以使用一个循环不断读取并处理缓冲区中的数据。可以通过循环读取队列中的数据,并对每个数据进行相应的处理操作,如输出到终端或者进行数据解析等。 最后,当不再需要接收串口数据时,需要关闭串口,并释放相应资源。 综上所述,通过使用环形队列缓冲,可以实现串口接收程序,有效地管理接收数据。这样可以提高数据处理的效率和程序的稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大橙子疯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值