What's the matter with the ring queue?

Posted by ziola on Tue, 21 Dec 2021 12:29:24 +0100

Two days ago, I saw a C language circular queue article by a friend author Fage: C language, ring queue , very well written. Most of the friends who have done actual projects have used the queue, which can be used to solve the problem of imbalance between output and consumption between producers and consumers. Take two cases I encountered in previous projects:

1) I need to display multiple events on one screen, and there are many sources of events and the trigger time is very fast, but the screen can not display all events synchronously due to resource constraints. At this time, you can use the queue to insert events into the queue, and the display program reads the events in the queue and displays them one by one.

2) My previous article: A summary of Bluetooth actual combat project For the Bluetooth transceiver mentioned in, on the one hand, the Bluetooth chip receives the data sent by the mobile phone, on the other hand, it should send the data through USB, but the interval between USB sending data is required to be relatively long, which can also be solved through queue.

There are ready-made queue functions in C + +, but the C language needs to be implemented by itself. I used a code on Github in my previous project, https://github.com/kuaileguyue/Ring-Buffer , simple and easy to use. It was used at that time. Because I probably knew the principle, I didn't seriously analyze the implementation details of the code. This time, I just saw a lot of comments and discussions on the ring queue under Fage's article. I looked at the code used before in detail and found that the harvest was still not small. I'll share it with you here.

First code:

ringbuffer.h

#include <inttypes.h>

/**
 * @file
 * Prototypes and structures for the ring buffer module.
 */

#ifndef RINGBUFFER_H
#define RINGBUFFER_H

/**
 * The size of a ring buffer.
 * Due to the design only <tt> RING_BUFFER_SIZE-1 </tt> items
 * can be contained in the buffer.
 * The buffer size must be a power of two.
*/
#define RING_BUFFER_SIZE 128

#if (RING_BUFFER_SIZE & (RING_BUFFER_SIZE - 1)) != 0
#error "RING_BUFFER_SIZE must be a power of two"
#endif

/**
 * The type which is used to hold the size
 * and the indicies of the buffer.
 * Must be able to fit \c RING_BUFFER_SIZE .
 */
typedef uint8_t ring_buffer_size_t;

/**
 * Used as a modulo operator
 * as <tt> a % b = (a & (b − 1)) </tt>
 * where \c a is a positive index in the buffer and
 * \c b is the (power of two) size of the buffer.
 */
#define RING_BUFFER_MASK (RING_BUFFER_SIZE-1)

/**
 * Simplifies the use of <tt>struct ring_buffer_t</tt>.
 */
typedef struct ring_buffer_t ring_buffer_t;

/**
 * Structure which holds a ring buffer.
 * The buffer contains a buffer array
 * as well as metadata for the ring buffer.
 */
struct ring_buffer_t {
  /** Buffer memory. */
  char buffer[RING_BUFFER_SIZE];
  /** Index of tail. */
  ring_buffer_size_t tail_index;
  /** Index of head. */
  ring_buffer_size_t head_index;
};

/**
 * Initializes the ring buffer pointed to by <em>buffer</em>.
 * This function can also be used to empty/reset the buffer.
 * @param buffer The ring buffer to initialize.
 */
void ring_buffer_init(ring_buffer_t *buffer);

/**
 * Adds a byte to a ring buffer.
 * @param buffer The buffer in which the data should be placed.
 * @param data The byte to place.
 */
void ring_buffer_queue(ring_buffer_t *buffer, char data);

/**
 * Adds an array of bytes to a ring buffer.
 * @param buffer The buffer in which the data should be placed.
 * @param data A pointer to the array of bytes to place in the queue.
 * @param size The size of the array.
 */
void ring_buffer_queue_arr(ring_buffer_t *buffer, const char *data, ring_buffer_size_t size);

/**
 * Returns the oldest byte in a ring buffer.
 * @param buffer The buffer from which the data should be returned.
 * @param data A pointer to the location at which the data should be placed.
 * @return 1 if data was returned; 0 otherwise.
 */
uint8_t ring_buffer_dequeue(ring_buffer_t *buffer, char *data);

/**
 * Returns the <em>len</em> oldest bytes in a ring buffer.
 * @param buffer The buffer from which the data should be returned.
 * @param data A pointer to the array at which the data should be placed.
 * @param len The maximum number of bytes to return.
 * @return The number of bytes returned.
 */
uint8_t ring_buffer_dequeue_arr(ring_buffer_t *buffer, char *data, ring_buffer_size_t len);
/**
 * Peeks a ring buffer, i.e. returns an element without removing it.
 * @param buffer The buffer from which the data should be returned.
 * @param data A pointer to the location at which the data should be placed.
 * @param index The index to peek.
 * @return 1 if data was returned; 0 otherwise.
 */
uint8_t ring_buffer_peek(ring_buffer_t *buffer, char *data, ring_buffer_size_t index);


/**
 * Returns whether a ring buffer is empty.
 * @param buffer The buffer for which it should be returned whether it is empty.
 * @return 1 if empty; 0 otherwise.
 */
inline uint8_t ring_buffer_is_empty(ring_buffer_t *buffer) {
  return (buffer->head_index == buffer->tail_index);
}

/**
 * Returns whether a ring buffer is full.
 * @param buffer The buffer for which it should be returned whether it is full.
 * @return 1 if full; 0 otherwise.
 */
inline uint8_t ring_buffer_is_full(ring_buffer_t *buffer) {
  return ((buffer->head_index - buffer->tail_index) & RING_BUFFER_MASK) == RING_BUFFER_MASK;
}

/**
 * Returns the number of items in a ring buffer.
 * @param buffer The buffer for which the number of items should be returned.
 * @return The number of items in the ring buffer.
 */
inline ring_buffer_size_t ring_buffer_num_items(ring_buffer_t *buffer) {
  return ((buffer->head_index - buffer->tail_index) & RING_BUFFER_MASK);
}

#endif /* RINGBUFFER_H */

ringbuffer.c

#include "ringbuffer.h"

/**
 * @file
 * Implementation of ring buffer functions.
 */

void ring_buffer_init(ring_buffer_t *buffer) {
  buffer->tail_index = 0;
  buffer->head_index = 0;
}

void ring_buffer_queue(ring_buffer_t *buffer, char data) {
  /* Is buffer full? */
  if(ring_buffer_is_full(buffer)) {
    /* Is going to overwrite the oldest byte */
    /* Increase tail index */
    buffer->tail_index = ((buffer->tail_index + 1) & RING_BUFFER_MASK);
  }

  /* Place data in buffer */
  buffer->buffer[buffer->head_index] = data;
  buffer->head_index = ((buffer->head_index + 1) & RING_BUFFER_MASK);
}

void ring_buffer_queue_arr(ring_buffer_t *buffer, const char *data, ring_buffer_size_t size) {
  /* Add bytes; one by one */
  ring_buffer_size_t i;
  for(i = 0; i < size; i++) {
    ring_buffer_queue(buffer, data[i]);
  }
}

ring_buffer_size_t ring_buffer_dequeue(ring_buffer_t *buffer, char *data) {
  if(ring_buffer_is_empty(buffer)) {
    /* No items */
    return 0;
  }
  
  *data = buffer->buffer[buffer->tail_index];
  buffer->tail_index = ((buffer->tail_index + 1) & RING_BUFFER_MASK);
  return 1;
}

ring_buffer_size_t ring_buffer_dequeue_arr(ring_buffer_t *buffer, char *data, ring_buffer_size_t len) {
  if(ring_buffer_is_empty(buffer)) {
    /* No items */
    return 0;
  }

  char *data_ptr = data;
  ring_buffer_size_t cnt = 0;
  while((cnt < len) && ring_buffer_dequeue(buffer, data_ptr)) {
    cnt++;
    data_ptr++;
  }
  return cnt;
}

ring_buffer_size_t ring_buffer_peek(ring_buffer_t *buffer, char *data, ring_buffer_size_t index) {
  if(index >= ring_buffer_num_items(buffer)) {
    /* No items at index */
    return 0;
  }
  
  /* Add index to pointer */
  ring_buffer_size_t data_index = ((buffer->tail_index + index) & RING_BUFFER_MASK);
  *data = buffer->buffer[data_index];
  return 1;
}

extern inline uint8_t ring_buffer_is_empty(ring_buffer_t *buffer);
extern inline uint8_t ring_buffer_is_full(ring_buffer_t *buffer);
extern inline uint8_t ring_buffer_num_items(ring_buffer_t *buffer);

The code adds up to only 200 lines, so I won't analyze it one by one. I'll focus on the following specific problems.

Question 1: how to initialize the ring queue?

Answer: just define a ring_buffer_t type structure, then call ring_. buffer_ Init() function can be initialized.

  ring_buffer_t ring_buffer;
  ring_buffer_init(&ring_buffer);

Question 2: how to set the queue length?

Answer: in ringbuffer The following macro definition settings in H. note that ring should be confirmed when modifying the length_ buffer_ size_ Type uint8 of T_ T whether it can be met, otherwise this type should be modified.

 #define RING_BUFFER_SIZE 128

Question 3: why must the ring queue length be to the nth power of 2?

Answer: ring is used to judge whether the queue is full_ BUFFER_ MASK, while ring_ BUFFER_ The value of MASK is RING_BUFFER_SIZE-1, this MASK is binary all 1, so the length is the nth power of 2.

 inline uint8_t ring_buffer_is_full(ring_buffer_t *buffer) {
  return ((buffer->head_index - buffer->tail_index) & RING_BUFFER_MASK) == RING_BUFFER_MASK;
}

Question 4: how does the code determine that the queue is full? What happens when it's full?

Answer: in ring_ buffer_ is_ In the full function, head is used_ Index minus tail_index, based on this value.

head_ The value of index is always + +, from 0 to RING_BUFFER_MASK, then go back to 0 and continue to add (form a ring). Only when ring is called_ buffer_ The queue () function will change only when it enters the queue, buffer - > head_ index = ((buffer->head_index + 1) & RING_BUFFER_MASK);

tail_ Index is different when calling ring_ buffer_ When the queue () function leaves the queue, it will + +, buffer - > tail_ index = ((buffer->tail_index + 1) & RING_ BUFFER_ MASK); Note: in ring_ buffer_ In the queue () function, when it is judged that the queue is full, it will also be added.

  if(ring_buffer_is_full(buffer)) {
    /* Is going to overwrite the oldest byte */
    /* Increase tail index */
    buffer->tail_index = ((buffer->tail_index + 1) & RING_BUFFER_MASK);
  }

How do you understand this? That is, when entering the queue, when it is judged that the queue is full, head_index is about to return to tail_ When the index position is, the tail will be_ Push the index forward. The effect is that the data that initially entered the queue is overwritten by the data that newly entered the queue, and there is no chance to be taken out again.

Look back at the sentence to determine whether it is full,

 return ((buffer->head_index - buffer->tail_index) & RING_BUFFER_MASK) == RING_BUFFER_MASK;

You can feel the brilliance of its writing, head_index is probably better than tail_index may be larger than tail_ It's small,

Take a queue with a length of 4 as an example. Suppose that it has been in the queue but not out of the queue. The whole process is as follows:

You can see when head_index =3,tail_index =0 or head_index =0,tail_index =1 or head_index =1,tail_index =2 or head_index =2,tail_ When index = 3, the queue is full.

I think it's good. Point a praise and support it!