Creación de un búfer Circular en C y C++

Actualizado: 20210321

Debido a la naturaleza de recursos limitados de los sistemas embebidos, las estructuras de datos de búfer circular se pueden encontrar en la mayoría de los proyectos.Los buffers circulares (también conocidos como buffers de anillo) son buffers de tamaño fijo que funcionan como si la memoria fuera contigua & de naturaleza circular. A medida que se genera y consume memoria, no es necesario reorganizar los datos, sino que se ajustan los punteros de cabeza/cola. Cuando se agregan datos, el puntero de la cabeza avanza. Cuando se consumen datos, el puntero de cola avanza. Si llega al final del búfer, los punteros simplemente se envuelven hasta el principio.

Para obtener un resumen más detallado de la operación del búfer circular, consulte el artículo de Wikipedia. El resto del artículo asume que tiene una comprensión de cómo funcionan los búferes circulares.

Tabla de contenido:

  1. ¿Por qué usar un Búfer Circular?
  2. Implementación en C
    1. Usando Encapsulación
    2. Diseño de API
    3. Determinar si un Búfer está lleno
    4. Tipo de contenedor de Búfer Circular
    5. Implementación
    6. Uso
    7. Modificaciones para Eliminar el id defull bandera
  3. Implementación en C++
    1. Definición de clase
    2. Implementación
    3. Uso
    4. Actualizaciones para C++17
  4. Uniéndolo Todo
  5. Lectura adicional

¿Por qué Usar Un Búfer Circular?

Los búferes circulares se utilizan a menudo como colas de tamaño fijo. El tamaño fijo es beneficioso para los sistemas integrados, ya que los desarrolladores a menudo intentan usar métodos de almacenamiento de datos estáticos en lugar de asignaciones dinámicas.

Los buffers circulares también son estructuras útiles para situaciones en las que la producción y el consumo de datos ocurren a diferentes velocidades: los datos más recientes siempre están disponibles. Si el consumidor no puede mantenerse al día con la producción, los datos obsoletos se sobrescribirán con datos más recientes. Al usar un búfer circular, podemos asegurarnos de que siempre consumimos los datos más recientes.

Para casos de uso adicionales, consulte Conceptos básicos de búfer de anillo en Embedded.com.

Implementación en C

Comenzaremos con una implementación en C, ya que esto nos expone a algunos de los desafíos de diseño y compensaciones al crear una biblioteca de búfer circular.

Usando Encapsulación

Dado que estamos creando una biblioteca de búfer circular, queremos asegurarnos de que los usuarios trabajen con nuestras API de biblioteca en lugar de modificar la estructura directamente. También queremos mantener la implementación contenida en nuestra biblioteca para que podamos cambiarla según sea necesario, sin requerir que los usuarios finales actualicen su código. El usuario no necesita conocer ningún detalle sobre nuestra estructura, solo que existe.

En el encabezado de nuestra biblioteca, declararemos la estructura:

// Opaque circular buffer structuretypedef struct circular_buf_t circular_buf_t;

No queremos que los usuarios trabajen con un puntero circular_but_t directamente, ya que podrían tener la impresión de que pueden desreferenciar el valor. Crearemos un tipo de identificador que pueden usar en su lugar.

El enfoque más simple para nuestro identificador es typedefel cbuf_handle_t como un puntero al búfer circular. Esto evitará que necesitemos lanzar el puntero dentro de nuestra implementación de funciones.

// Handle type, the way users interact with the APItypedef circular_buf_t* cbuf_handle_t;

Un enfoque alternativo sería que el mango de un uintptr_t o void* valor. Dentro de nuestra interfaz, manejaríamos la traducción al tipo de puntero apropiado. Mantenemos el tipo de búfer circular oculto a los usuarios, y la única forma de interactuar con los datos es a través del controlador.

Vamos a seguir con la implementación de simple handle para mantener nuestro código de ejemplo simple y directo.

Diseño de API

Primero, debemos pensar en cómo los usuarios interactuarán con un búfer circular:

  • Necesitan inicializar el contenedor de búfer circular con un búfer y tamaño
  • Necesitan destruir un contenedor de búfer circular
  • Necesitan restablecer el contenedor de búfer circular
  • Necesitan poder agregar datos al búfer
  • Necesitan poder obtener el siguiente valor del búfer
  • Necesitan saber si el búfer está lleno o vacío
  • Necesitan saber el número actual de elementos en el búfer
  • Necesitan conocer la capacidad máxima del búfer

Usando esta lista, podemos armar una API para nuestra biblioteca. Los usuarios interactuarán con la biblioteca de búfer circular utilizando nuestro tipo de identificador opaco, que se crea durante la inicialización.

he elegido uint8_t como el tipo de datos subyacente en esta aplicación. Puede usar cualquier tipo en particular que desee, solo tenga cuidado de manejar el búfer subyacente y el número de bytes de manera adecuada.

/// Pass in a storage buffer and size /// Returns a circular buffer handlecbuf_handle_t circular_buf_init(uint8_t* buffer, size_t size);/// Free a circular buffer structure./// Does not free data buffer; owner is responsible for thatvoid circular_buf_free(cbuf_handle_t cbuf);/// Reset the circular buffer to empty, head == tailvoid circular_buf_reset(cbuf_handle_t cbuf);/// Put version 1 continues to add data if the buffer is full/// Old data is overwrittenvoid circular_buf_put(cbuf_handle_t cbuf, uint8_t data);/// Put Version 2 rejects new data if the buffer is full/// Returns 0 on success, -1 if buffer is fullint circular_buf_put2(cbuf_handle_t cbuf, uint8_t data);/// Retrieve a value from the buffer/// Returns 0 on success, -1 if the buffer is emptyint circular_buf_get(cbuf_handle_t cbuf, uint8_t * data);/// Returns true if the buffer is emptybool circular_buf_empty(cbuf_handle_t cbuf);/// Returns true if the buffer is fullbool circular_buf_full(cbuf_handle_t cbuf);/// Returns the maximum capacity of the buffersize_t circular_buf_capacity(cbuf_handle_t cbuf);/// Returns the current number of elements in the buffersize_t circular_buf_size(cbuf_handle_t cbuf);

Determinar si un búfer está lleno

Antes de proceder, deberíamos tomarnos un momento para discutir el método que usaremos para determinar si el búfer está lleno o vacío.

Los casos «lleno» y «vacío»del búfer circular se ven iguales: head y tail el puntero es igual. Existen dos enfoques para diferenciar entre lleno y vacío:

  1. «Residuos» de una ranura en el búfer:
    • Completo estado de head + 1 == tail
    • estado Vacío es head == tail
  2. el Uso de un bool bandera y lógica adicional para diferenciar los estados::
    • Completo estado de full
    • estado Vacío es (head == tail) && !full

también Debemos considerar la seguridad de los subprocesos. Al usar una sola celda vacía para detectar el caso «lleno», podemos admitir un solo productor y un solo consumidor sin bloqueo (siempre que put y get no modifiquen las mismas variables). La cola es segura para subprocesos porque el productor solo modificará el índice head, y el consumidor solo modificará el índice tail. Si bien cualquiera de los índices puede estar un poco desactualizado en un contexto determinado, esto no afectará a la seguridad del subproceso de la cola. Sin embargo, el uso del indicador full crea un requisito de exclusión mutua. Esto se debe a que el indicador full es compartido por el productor y el consumidor.

por supuesto, la decisión tiene sus desventajas. Si el elemento de búfer ocupa una gran cantidad de memoria (como un búfer del tamaño de un i-frame de cámara), es posible que desperdiciar una ranura no sea razonable en el sistema. Si tiene varios productores/consumidores interactuando con una cola, necesitará un candado de todos modos, por lo que desperdiciar una ranura no tiene sentido. Si no tiene exclusión mutua disponible (por ejemplo, porque no está utilizando un sistema operativo) pero está utilizando interrupciones, entonces querrá usar la versión de bandera que no esfull. El modelo de memoria utilizado en su sistema también puede tener un impacto en su decisión de ir sin cerradura.

La implementación a continuación utiliza el indicador bool. El uso del indicador requiere lógica adicional en las rutinas get y put para actualizar el indicador. También describiré cómo hacer las modificaciones para un solo productor / consumidor que no use la bandera full.

Tipo de contenedor de búfer circular

Ahora que tenemos una idea de las operaciones que necesitaremos soportar, podemos diseñar nuestro contenedor de búfer circular.

Utilizamos la estructura del contenedor para administrar el estado del búfer. Para preservar la encapsulación, la estructura del contenedor se define dentro del archivo .c de nuestra biblioteca, en lugar de en el encabezado.

Tendremos que hacer un seguimiento de:

  • El búfer de datos subyacente
  • El tamaño máximo del búfer
  • La posición actual de «cabeza» (incrementada cuando se agregan elementos)
  • La «cola» actual (incrementada cuando se eliminan elementos)
  • Una bandera que indica si el búfer está lleno o no
// The hidden definition of our circular buffer structurestruct circular_buf_t {uint8_t * buffer;size_t head;size_t tail;size_t max; //of the bufferbool full;};

Ahora que nuestro contenedor está diseñado, estamos listos para implementar las funciones de biblioteca.

Implementación

Un detalle importante a tener en cuenta es que cada una de nuestras API requiere un controlador de búfer inicializado. En lugar de ensuciar nuestro código con sentencias condicionales, utilizaremos aserciones para hacer cumplir nuestros requisitos de API en el estilo «Diseño por contrato».

Si las interfaces se utilizan incorrectamente, el programa fallará inmediatamente en lugar de requerir que el usuario verifique y maneje el código de error.

Por ejemplo:

circular_buf_reset(NULL);

Produce:

=== C Circular Buffer Check ===Assertion failed: (cbuf), function circular_buf_reset, file ../../circular_buffer.c, line 35.Abort trap: 6

Otra nota importante es que la implementación que se muestra a continuación no es segura para subprocesos. No se han añadido bloqueos a la biblioteca de búfer circular subyacente.

Inicializar y restablecer

Empecemos por el principio: inicializando un búfer circular. Nuestra API tiene clientes que proporcionan el búfer subyacente y el tamaño del búfer, y les devolvemos un controlador de búfer circular. La razón por la que queremos que nuestros usuarios proporcionen el búfer es que esto permite un búfer asignado estáticamente. Si nuestra API creara el búfer bajo el capó, necesitaríamos hacer uso de la asignación de memoria dinámica, que a menudo no se permite en los programas de sistemas embebidos.

Se nos requiere proporcionar una instancia de estructura de búfer circular dentro de la biblioteca para que podamos devolver un puntero al usuario. He utilizado malloc para simplificar. Los sistemas que no pueden usar memoria dinámica simplemente necesitan modificar la función init para usar un método diferente, como la asignación de un grupo estático de estructuras de búfer circulares preasignadas.

Otro enfoque sería romper la encapsulación, permitiendo a los usuarios declarar estáticamente estructuras de contenedores de búfer circular fuera de la biblioteca. En este caso, circular_buf_init necesita actualizarse para tomar un puntero de estructura. También podríamos tener nuestra función init crear una estructura de contenedor en la pila y devolverla al por mayor. Sin embargo, dado que la encapsulación está rota, los usuarios podrán modificar la estructura sin usar las rutinas de la biblioteca. Si desea conservar la encapsulación, debe trabajar con punteros en lugar de instancias de estructura de concreto.

// User provides structvoid circular_buf_init(circular_buf_t* cbuf, uint8_t* buffer, size_t size);// Return a concrete structcircular_buf_t circular_buf_init(uint8_t* buffer, size_t size);// Return a pointer to a struct instance - preferred approachcbuf_handle_t circular_buf_init(uint8_t* buffer, size_t size);

Devolveremos un identificador a una estructura que está asignada dentro de la biblioteca. Una vez que hayamos creado nuestro contenedor, necesitamos rellenar los valores y llamar a reset en él. Antes de volver de init, nos aseguramos de que el contenedor de búfer se haya creado en un estado vacío.

cbuf_handle_t circular_buf_init(uint8_t* buffer, size_t size){assert(buffer && size);cbuf_handle_t cbuf = malloc(sizeof(circular_buf_t));assert(cbuf);cbuf->buffer = buffer;cbuf->max = size;circular_buf_reset(cbuf);assert(circular_buf_empty(cbuf));return cbuf;}

El propósito de la función de reset, es poner el colchón en un «vacío» del estado, que requiere la actualización de headtail y full:

void circular_buf_reset(cbuf_handle_t cbuf){ assert(cbuf); cbuf->head = 0; cbuf->tail = 0; cbuf->full = false;}

Dado que tenemos un método para crear un contenedor de búfer circular, necesitamos un método equivalente para destruir el contenedor. En este caso, llamamos a free en nuestro contenedor. No intentamos liberar el búfer subyacente, ya que no lo poseemos.

void circular_buf_free(cbuf_handle_t cbuf){assert(cbuf);free(cbuf);}

Comprobaciones de estado

A continuación, implementaremos las funciones relacionadas con el estado del contenedor de búfer.

La función completa es la más fácil de implementar, ya que tenemos una bandera que representa el estado:

bool circular_buf_full(cbuf_handle_t cbuf){assert(cbuf); return cbuf->full;}

Dado que tenemos la bandera full para diferenciar entre estado lleno o vacío, combinamos la bandera con una comprobación de que head == tail:

bool circular_buf_empty(cbuf_handle_t cbuf){assert(cbuf); return (!cbuf->full && (cbuf->head == cbuf->tail));}

la capacidad de nuestro búfer se suministró durante la inicialización, por lo que simplemente devolvemos ese valor al usuario:

size_t circular_buf_capacity(cbuf_handle_t cbuf){assert(cbuf);return cbuf->max;}

Calcular el número de elementos en el búfer fue un problema más complicado de lo que esperaba. Muchos cálculos de tamaño propuestos usan módulo, pero me encontré con extraños casos de esquina cuando los probé. Opté por un cálculo simplificado utilizando declaraciones condicionales.

Si el búfer está lleno, sabemos que nuestra capacidad está al máximo. Si head es mayor o igual que tail, simplemente restamos los dos valores para obtener nuestro tamaño. Si tail mayor que head, se necesita compensar la diferencia con la etiqueta max para obtener el tamaño correcto.

size_t circular_buf_size(cbuf_handle_t cbuf){assert(cbuf);size_t size = cbuf->max;if(!cbuf->full){if(cbuf->head >= cbuf->tail){size = (cbuf->head - cbuf->tail);}else{size = (cbuf->max + cbuf->head - cbuf->tail);}}return size;}

Agregar y eliminar datos

Con las funciones de contabilidad fuera del camino, es hora de profundizar en la carne: agregar y eliminar datos de la cola.

Agregar y eliminar datos de un búfer circular requiere la manipulación de los punteros head y tail. Al agregar datos al búfer, insertamos el nuevo valor en la ubicación actual head, luego avanzamos head. Cuando eliminamos datos del búfer, recuperamos el valor del puntero actual tail y luego avanzamos tail.Sin embargo, agregar datos al búfer requiere un poco más de reflexión. Si el buffer está full, necesitamos avanzar en nuestra tail puntero así como head. También necesitamos comprobar si insertar un valor activa la condición full.

Vamos a implementar dos versiones de la función put, así que vamos a extraer nuestra lógica de avance de puntero en una función auxiliar. Si nuestro búfer ya está lleno, avanzamos tail. Siempre avanzamos head por uno. Una vez avanzado el puntero, rellenamos la bandera full comprobando si head == tail.

Observe el uso del operador de módulo (%) a continuación. Modulo hará que los valores head y tail se restablezcan a 0 cuando se alcance el tamaño máximo. Esto garantiza que heady tail sean siempre índices válidos del búfer de datos subyacente.

static void advance_pointer(cbuf_handle_t cbuf){assert(cbuf);if(cbuf->full) {cbuf->tail = (cbuf->tail + 1) % cbuf->max;}cbuf->head = (cbuf->head + 1) % cbuf->max;cbuf->full = (cbuf->head == cbuf->tail);}

Como bien señaló Miro Samek, esta es una operación computacional costosa. En su lugar, podemos usar lógica condicional para reducir el número total de instrucciones. Miro el enfoque recomendado es:

if (++(cbuf->head) == cbuf->max) { cbuf->head = 0;}

Ahora advance_pointer tendrá este aspecto:

static void advance_pointer(cbuf_handle_t cbuf){assert(cbuf);if(cbuf->full) {if (++(cbuf->tail) == cbuf->max) { cbuf->tail = 0;}}if (++(cbuf->head) == cbuf->max) { cbuf->head = 0;}cbuf->full = (cbuf->head == cbuf->tail);}

Podemos hacer una función auxiliar similar a la que se llama al eliminar un valor del búfer. Cuando eliminamos un valor, el indicador full se establece en false, y el puntero de cola es avanzado.

static void retreat_pointer(cbuf_handle_t cbuf){assert(cbuf);cbuf->full = false;if (++(cbuf->tail) == cbuf->max) { cbuf->tail = 0;}}

Crearemos dos versiones de la función put. La primera versión inserta un valor en el búfer y avanza el puntero. Si el búfer está lleno, se sobrescribirá el valor más antiguo. Este es el caso de uso estándar para un búfer circular

void circular_buf_put(cbuf_handle_t cbuf, uint8_t data){assert(cbuf && cbuf->buffer); cbuf->buffer = data; advance_pointer(cbuf);}

La segunda versión de la función put devuelve un error si el búfer está lleno. Esto se proporciona con fines de demostración, pero no utilizamos esta variante en nuestros sistemas.

int circular_buf_put2(cbuf_handle_t cbuf, uint8_t data){ int r = -1; assert(cbuf && cbuf->buffer); if(!circular_buf_full(cbuf)) { cbuf->buffer = data; advance_pointer(cbuf); r = 0; } return r;}

Para eliminar datos del búfer, accedemos al valor en el puntero tail y luego actualizamos el puntero tail. Si el búfer está vacío, no devolvemos un valor ni modificamos el puntero. En su lugar, devolvemos un error al usuario.

int circular_buf_get(cbuf_handle_t cbuf, uint8_t * data){ assert(cbuf && data && cbuf->buffer); int r = -1; if(!circular_buf_empty(cbuf)) { *data = cbuf->buffer; retreat_pointer(cbuf); r = 0; } return r;}

Que completa la implementación de nuestra biblioteca de búfer circular.

Uso

Cuando se utiliza la biblioteca, el cliente es responsable de crear el búfer de datos subyacente a circular_buf_init, y se devuelve un cbuf_handle_t:

uint8_t * buffer = malloc(EXAMPLE_BUFFER_SIZE * sizeof(uint8_t));cbuf_handle_t cbuf = circular_buf_init(buffer, EXAMPLE_BUFFER_SIZE);

Este identificador se utiliza para interactuar con todas las funciones restantes de la biblioteca:

bool full = circular_buf_full(cbuf);bool empty = circular_buf_empty(cbuf);printf("Current buffer size: %zu\n", circular_buf_size(cbuf);

No olvide liberar el búfer de datos subyacente y el contenedor cuando haya terminado:

free(buffer);circular_buf_free(cbuf);

Se puede encontrar un programa de prueba que utiliza la biblioteca de búfer circular en el repositorio de recursos incrustados.

Modificaciones para Eliminar la bandera completa

Si desea deshacerse de la bandera full, en su lugar, debe verificar que head está una posición detrás de la cola para determinar si el búfer está lleno:

bool circular_buf_full(circular_buf_t* cbuf){// We determine "full" case by head being one position behind the tail// Note that this means we are wasting one space in the buffer return ((cbuf->head + 1) % cbuf->size) == cbuf->tail;}

Ahora, si queremos evitar la operación de módulo, podemos usar lógica condicional en su lugar:

bool circular_buf_full(circular_buf_t* cbuf){// We need to handle the wraparound case size_t head = cbuf->head + 1; if(head == cbuf->max) {head = 0; }return head == cbuf->tail;}

El caso vacío es entonces que head y tail son los mismos:

bool circular_buf_empty(circular_buf_t* cbuf){// We define empty as head == tail return (cbuf->head == cbuf->tail);}

Al obtener los datos del búfer, avanzaremos puntero de cola, envolviendo si es necesario:

int circular_buf_get(circular_buf_t * cbuf, uint8_t * data){ int r = -1; if(cbuf && data && !circular_buf_empty(cbuf)) { *data = cbuf->buffer; cbuf->tail = (cbuf->tail + 1) % cbuf->size; r = 0; } return r;}

Al agregar datos al búfer, almacenaremos los datos y avanzaremos el puntero de cabeza, envolviendo si es necesario:

int circular_buf_put(circular_buf_t * cbuf, uint8_t data){ int r = -1; if(cbuf && !circular_nuf_full(cbuf)) { cbuf->buffer = data; cbuf->head = (cbuf->head + 1) % cbuf->size; r = 0; } return r;}

Se pueden eliminar otras referencias a full.

C++

C++ se presta a una implementación de búfer circular más limpia que C.

Definición de clase

Comenzaremos definiendo nuestra clase C++. Queremos que nuestra implementación de C++ admita cualquier tipo de datos, por lo que vamos a convertirla en una clase con plantilla.

Nuestras API serán similares a la implementación de C. Nuestra clase proporcionará interfaces para:

  • Restablecer el búfer a vacío
  • Agregar datos
  • Eliminar datos
  • Comprobar el estado lleno/vacío
  • Comprobar el número actual de elementos en el búfer
  • Comprobar la capacidad total del búfer

También utilizaremos punteros inteligentes de C++ para asegurarnos de que no dejamos ningún dato una vez que se destruye nuestro búfer. Esto significa que podemos administrar el búfer para el usuario.

Otro beneficio de C++ es la trivialidad de hacer que esta clase sea segura para subprocesos: podemos confiar en el tipo std::mutex (suponiendo que esto esté definido para su plataforma).

Aquí está nuestra definición de clase:

template <class T>class circular_buffer {public:explicit circular_buffer(size_t size) :buf_(std::unique_ptr<T>(new T)),max_size_(size){ // empty }void put(T item);T get();void reset();bool empty() const;bool full() const;size_t capacity() const;size_t size() const;private:std::mutex mutex_;std::unique_ptr<T> buf_;size_t head_ = 0;size_t tail_ = 0;const size_t max_size_;bool full_ = 0;};

Implementación de C++

Nuestro búfer circular de C++ imita gran parte de la lógica de la implementación de C, pero da como resultado un diseño mucho más limpio y reutilizable. Además, el búfer de C++ utiliza std::mutex para proporcionar una implementación segura para subprocesos.

Nota: Se pueden realizar los mismos cambios lógicos en la implementación de C++ para admitir la seguridad de subprocesos con un solo productor y consumidor al «desperdiciar» una ranura. Consulte los ajustes en la implementación de C para obtener más información.

Inicialización

Al construir nuestra clase, asignamos los datos para nuestro búfer subyacente y establecemos el tamaño del búfer. Esto elimina la sobrecarga requerida con la implementación de C.

A diferencia de la implementación de C, el constructor de C++ no llama a reset. Debido a que especificamos valores iniciales para nuestras variables miembro, nuestro búfer circular comienza en el estado correcto.

explicit circular_buffer(size_t size) :buf_(std::unique_ptr<T>(new T)),max_size_(size){//empty constructor}

Nuestro comportamiento de restablecimiento pone el búfer de nuevo a un estado vacío (head == tail && !full_).

void reset(){std::lock_guard<std::mutex> lock(mutex_);head_ = tail_;full_ = false;}

Seguimiento de estado

La lógica de los casos empty y full es la misma que el ejemplo de C:

bool empty() const{//if head and tail are equal, we are emptyreturn (!full_ && (head_ == tail_));}bool full() const{//If tail is ahead the head by 1, we are fullreturn full_;}

En la implementación de búfer circular de C++, size y capacity informan el número de elementos en la cola en lugar del tamaño en bytes. Esto nos permite ser agnósticos a los detalles subyacentes del tipo.

size_t capacity() const{return max_size_;}size_t size() const{size_t size = max_size_;if(!full_){if(head_ >= tail_){size = head_ - tail_;}else{size = max_size_ + head_ - tail_;}}return size;}

Agregar Datos

La lógica de put coincide con la implementación de C. Esta implementación utiliza el patrón de comportamiento «sobrescribir el valor más antiguo».

void put(T item){std::lock_guard<std::mutex> lock(mutex_);buf_ = item;if(full_){tail_ = (tail_ + 1) % max_size_;}head_ = (head_ + 1) % max_size_;full_ = head_ == tail_;}

Nota: Por simplicidad, he omitido la opción que evita las operaciones de módulo. Puedes encontrar esa lógica en la sección C.

Recuperación de datos

La lógica detrás de get coincide con la implementación de C. A diferencia de la implementación de C, se devuelve un valor vacío si el búfer está vacío.

T get(){std::lock_guard<std::mutex> lock(mutex_);if(empty()){return T();}//Read data and advance the tail (we now have a free space)auto val = buf_;full_ = false;tail_ = (tail_ + 1) % max_size_;return val;}

tenga en cuenta: return T() devolverá un valor construido predeterminado para el tipo de ra dado. El valor real producido depende del tipo o del constructor. Además, por simplicidad, he omitido la opción que evita las operaciones de módulo. Puedes encontrar esa lógica en la sección C.

Uso

El búfer circular de C++ es mucho más sencillo de usar que la implementación de C.

Para crear una instancia de un búfer circular, simplemente declaramos un objeto y especificamos el tipo de plantilla para nuestro búfer. Este es un ejemplo que usa un búfer de 10 entradas uint32_t :

circular_buffer<uint32_t> circle(10);

Agregar datos es fácil:

uint32_t x = 100;circle.put(x);

Y obtener datos es igualmente fácil:

x = circle.get()

Recuerde que, dado que esta es una clase con plantilla, puede crear un búfer circular de cualquier tipo que necesite.

Actualizaciones para C++17

En C++17, tenemos acceso a std::optional, que nos permite representar un valor que puede o no estar presente. Nuestro get función devolverá un std::optional<T>. También devolveríamos std::nullopten lugar de un T construido por defecto si la cola está vacía.

std::optional<T> get(){std::lock_guard<std::mutex> lock(mutex_);if(empty()){return std::nullopt;}//Read data and advance the tail (we now have a free space)auto val = buf_;full_ = false;tail_ = (tail_ + 1) % max_size_;return val;}

Nota: Por simplicidad, he omitido la opción que evita las operaciones de módulo. Puedes encontrar esa lógica en la sección C.

En el código de llamada, puede verificar un valor válido utilizando el operador booleano o la función miembro has_value. Si hay un valor válido, se puede acceder a él utilizando los operadores -> o *, ur utilizando la función miembro value().

// Returns an optionalauto item = cbuf.get();// Check if the optional is validif(item){process_data(*item); // access the value}

Poniendo Todo Junto

Ejemplo implementaciones pueden encontrarse en la etiqueta embedded-resources repositorio de Github.

  • Programa de prueba de búfer circular de C
    • Biblioteca de búfer circular de C
  • Ejemplo de búfer circular de C++

Si está buscando ampliar esta biblioteca, un ejercicio útil es agregar API adicionales para permitir que los usuarios agreguen / eliminen varios elementos con una sola operación. También puede hacer que el hilo de implementación de C sea seguro.

Seguridad de los hilos con el Método Lookahead

Un enfoque para la seguridad de los hilos sin un mutex es el método «lookahead». Este método admite un solo hilo productor y un solo hilo consumidor; varios productores o consumidores requerirán un candado.

En lugar de usar la bandera booleana para diferenciar entre los casos llenos y vacíos, siempre dejaremos una celda vacía. Al usar una sola celda vacía para detectar el caso «lleno», podemos admitir un solo productor y un solo consumidor sin bloqueo (siempre que put y get no modifiquen las mismas variables).

Es posible que le preocupe perder una ranura, pero esta compensación a menudo es mucho más barata que el costo de usar una primitiva de bloqueo del sistema operativo.

Leer Más

  • C++ Punteros Inteligentes
  • Zanja C-Orzuelo Punteros de Punteros Inteligentes

Aquí hay otros búfer circular implementaciones:

  • QuantumLeaps/lock-gratis-anillo-buffer
  • willemt/BipBuffer

Para obtener más información en la circular búferes:

  • Embedded.com: búfer de Anillo conceptos básicos
  • Wikipedia: buffer Circular
  • Férreos Sistemas: Bloqueo del Anillo sin Búfer
  • Aumentar Búfer Circular
  • C++: Rendimiento de un Búfer circular frente a Vector, Desque y Lista
  • Cola Circular Sin bloqueo de un solo Productor y un solo Consumidor

Existe una propuesta para agregar un tipo de búfer circular a la biblioteca estándar de C++:

  • P0059: Una Propuesta para Agregar un Intervalo de anillos a la Biblioteca estándar
    • Intervalo de anillos
    • Intervalo de anillos Lite

Registro de cambios

  • 20210321
    • Se corrigió el error con max_size_ donde cbuf->max se ha utilizado
    • Se ha añadido texto aclaratorio con respecto a por qué el usuario pasa el búfer
  • 20210301
    • Se han añadido comentarios de Miro con respecto a evitar operaciones de módulo
  • 20210213
    • Se han añadido enlaces adicionales
    • Se ha añadido una discusión adicional sobre el equilibrio entre la bandera completa y el uso slot
    • Mostrar modificaciones para la seguridad de los hilos con un solo productor/consumidor
    • Añadir notas enstd::optional usar con C++17
  • 20191016
    • Formato actualizado de la sección de registro de cambios para mayor coherencia en todo el sitio
    • Encabezados degradados para mayor coherencia en todo el sitio
    • Enlaces fijos de tabla de contenidos rotos
    • Se eliminó el Registro de cambios de la Tabla de contenido
  • 20190627
    • Se agregó un enlace al artículo de Búfer de anillo sin bloqueo de Ferrous Systems.
  • 20190604
    • Corregido un error tipográfico (gracias Chris Svec!) y cambió algunas palabras relacionadas con el tipo opaco.
  • 20181219
    • Se agregó una nota sobre cómo evitar problemas de concurrencia con un solo productor y un solo consumidor que usan una ranura vacía.
  • 20180804
    • El artículo fue reestructurado y volver a escribir.Gracias a todos los que proporcionaron comentarios en el camino. Los ejemplos se han actualizado a:
    • Eliminar programación defensiva
    • Usar aserciones
    • Crear una biblioteca independiente utilizando una estructura opaca
    • Expandir las API, incluido un cálculo para el tamaño de búfer circular actual
    • Actualizar la biblioteca para que no desperdicie una ranura

Deja una respuesta

Tu dirección de correo electrónico no será publicada.