Aktualisiert: 20210321
Aufgrund der Ressourcenbeschränkung eingebetteter Systeme finden sich in den meisten Projekten Datenstrukturen für kreisförmige Puffer.
Kreisförmige Puffer (auch als Ringpuffer bezeichnet) sind Puffer fester Größe, die so funktionieren, als ob der Speicher zusammenhängend wäre & kreisförmiger Natur. Wenn Speicher generiert und verbraucht wird, müssen die Daten nicht neu gemischt werden, sondern die Head / Tail–Zeiger werden angepasst. Wenn Daten hinzugefügt werden, rückt der Kopfzeiger vor. Wenn Daten verbraucht werden, wird der Endzeiger vorgerückt. Wenn Sie das Ende des Puffers erreichen, werden die Zeiger einfach an den Anfang gewickelt.
Eine detailliertere Zusammenfassung der zirkulären Pufferoperation finden Sie im Wikipedia-Artikel. Der Rest des Artikels setzt voraus, dass Sie wissen, wie zirkuläre Puffer funktionieren.
Inhaltsverzeichnis:
- Warum einen zirkulären Puffer verwenden?
- C-Implementierung
- Kapselung verwenden
- API-Design
- Bestimmen, ob ein Puffer voll ist
- Typ des kreisförmigen Puffercontainers
- Implementierung
- Verwendung
- Änderungen zum Entfernen des
full
Flags
- C++ -Implementierung
- Klassendefinition
- Implementierung
- Verwendung
- Updates für C ++ 17
- Alles zusammenstellen
- Weiterführende Literatur
Warum einen kreisförmigen Puffer verwenden?
Kreisförmige Puffer werden häufig als Warteschlangen mit fester Größe verwendet. Die feste Größe ist für eingebettete Systeme von Vorteil, da Entwickler häufig versuchen, statische Datenspeichermethoden anstelle dynamischer Zuordnungen zu verwenden.
Zirkuläre Puffer sind auch nützliche Strukturen für Situationen, in denen Datenproduktion und -verbrauch unterschiedlich schnell ablaufen: Die neuesten Daten sind immer verfügbar. Wenn der Verbraucher nicht mit der Produktion Schritt halten kann, werden die veralteten Daten mit neueren Daten überschrieben. Durch die Verwendung eines kreisförmigen Puffers können wir sicherstellen, dass wir immer die neuesten Daten verbrauchen.
Weitere Anwendungsfälle finden Sie unter Ring Buffer Basics auf Embedded.com.
C-Implementierung
Wir werden mit einer C-Implementierung beginnen, da dies uns einigen Designherausforderungen und Kompromissen beim Erstellen einer zirkulären Pufferbibliothek aussetzt.
Kapselung verwenden
Da wir eine zirkuläre Pufferbibliothek erstellen, möchten wir sicherstellen, dass Benutzer mit unseren Bibliotheks-APIs arbeiten, anstatt die Struktur direkt zu ändern. Wir möchten auch, dass die Implementierung in unserer Bibliothek enthalten bleibt, damit wir sie nach Bedarf ändern können, ohne dass Endbenutzer ihren Code aktualisieren müssen. Der Benutzer muss keine Details über unsere Struktur wissen, nur dass sie existiert.
In unserem Bibliotheks-Headerdeklarieren wir dann die Struktur:
// Opaque circular buffer structuretypedef struct circular_buf_t circular_buf_t;
Wir möchten nicht, dass Benutzer direkt mit einem circular_but_t
Zeiger arbeiten, da sie den Eindruck haben könnten, dass sie den Wert dereferenzieren können. Wir erstellen einen Handle-Typ, den sie stattdessen verwenden können.
Der einfachste Ansatz für unser Handle ist typedef
die cbuf_handle_t
als Zeiger auf den kreisförmigen Puffer. Dies verhindert, dass wir den Zeiger innerhalb unserer Funktionsimplementierung umwandeln müssen.
// Handle type, the way users interact with the APItypedef circular_buf_t* cbuf_handle_t;
Ein alternativer Ansatz wäre, das Handle zu einem uintptr_t
oder void*
Wert zu machen. Innerhalb unserer Schnittstelle würden wir die Übersetzung in den entsprechenden Zeigertyp übernehmen. Wir halten den kreisförmigen Puffertyp für Benutzer verborgen, und die einzige Möglichkeit, mit den Daten zu interagieren, ist über das Handle.
Wir bleiben bei der simple Handle-Implementierung, um unseren Beispielcode einfach und unkompliziert zu halten.
API-Design
Zunächst sollten wir darüber nachdenken, wie Benutzer mit einem kreisförmigen Puffer interagieren:
- Sie müssen den kreisförmigen Puffercontainer mit einem Puffer und einer Größe initialisieren
- Sie müssen einen kreisförmigen Puffercontainer zerstören
- Sie müssen den kreisförmigen Puffercontainer zurücksetzen
- Sie müssen in der Lage sein, dem Puffer Daten hinzuzufügen
- Sie müssen in der Lage sein, den nächsten Wert aus dem Puffer abzurufen
- Sie müssen wissen, ob der Puffer voll oder leer ist
- Sie müssen die aktuelle Anzahl der Elemente im Puffer
- müssen sie die maximale Kapazität des Puffers kennen
Anhand dieser Liste können wir eine API für unsere Bibliothek zusammenstellen. Benutzer interagieren mit der zirkulären Pufferbibliothek mithilfe unseres undurchsichtigen Handle-Typs, der während der Initialisierung erstellt wird.
Ich habe uint8_t
als zugrunde liegenden Datentyp in dieser Implementierung ausgewählt. Sie können einen bestimmten Typ verwenden, den Sie mögen – achten Sie nur darauf, den zugrunde liegenden Puffer und die Anzahl der Bytes entsprechend zu behandeln.
/// 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);
Bestimmen, ob ein Puffer voll ist
Bevor wir fortfahren, sollten wir uns einen Moment Zeit nehmen, um die Methode zu besprechen, mit der wir feststellen, ob ein Puffer voll oder leer ist.
Sowohl die „vollen“ als auch die „leeren“ Fälle des kreisförmigen Puffers sehen gleich aus: head
und tail
Zeiger sind gleich. Es gibt zwei Ansätze, um zwischen voll und leer zu unterscheiden:
- „Verschwenden“ Sie einen Steckplatz im Puffer:
- Voller Zustand ist
head + 1 == tail
- Leerer Zustand ist
head == tail
- Voller Zustand ist
- Verwenden Sie eine
bool
flag und zusätzliche Logik zur Unterscheidung von Zuständen::- Vollständiger Zustand ist
full
- Leerer Zustand ist
(head == tail) && !full
- Vollständiger Zustand ist
Wir sollten auch die Thread-Sicherheit berücksichtigen. Indem wir eine einzelne leere Zelle verwenden, um den „vollen“ Fall zu erkennen, können wir einen einzelnen Produzenten und einen einzelnen Verbraucher ohne Sperre unterstützen (solange put
und get
nicht die gleichen Variablen ändern). Die Warteschlange ist threadsicher, da der Producer nur den head
Index ändert und der Consumer nur den tail
Index . Obwohl jeder Index in einem bestimmten Kontext möglicherweise etwas veraltet ist, wirkt sich dies nicht auf die Thread-Sicherheit der Warteschlange aus. Die Verwendung des full
Flags führt jedoch zu einer Anforderung für den gegenseitigen Ausschluss. Dies liegt daran, dass das Flag full
sowohl vom Produzenten als auch vom Konsumenten gemeinsam genutzt wird.
Natürlich hat die Entscheidung ihre Nachteile. Wenn Ihr Pufferelement einen großen Speicherbedarf hat (z. B. einen Puffer mit der Größe eines Kamera-I-Frames), ist die Verschwendung eines Steckplatzes auf Ihrem System möglicherweise nicht sinnvoll. Wenn mehrere Produzenten / Konsumenten mit einer Warteschlange interagieren, benötigen Sie sowieso eine Sperre, sodass es keinen Sinn macht, einen Slot zu verschwenden. Wenn Sie keinen gegenseitigen Ausschluss zur Verfügung haben (z. B. weil Sie kein Betriebssystem verwenden), aber Interrupts verwenden, sollten Sie die Nicht-full
Flag-Version verwenden. Das auf Ihrem System verwendete Speichermodell kann sich auch auf Ihre Entscheidung auswirken, auf eine Sperre zu verzichten.
Die folgende Implementierung verwendet das bool
Flag. Die Verwendung des Flags erfordert zusätzliche Logik in den Routinen get
und put
, um das Flag zu aktualisieren. Ich werde auch beschreiben, wie die Änderungen für einen einzelnen Produzenten / Konsumenten vorgenommen werden, der das full
Flag nicht verwendet.
Typ des kreisförmigen Puffercontainers
Nachdem wir nun die Operationen verstanden haben, die wir unterstützen müssen, können wir unseren kreisförmigen Puffercontainer entwerfen.
Wir verwenden die Containerstruktur, um den Zustand des Puffers zu verwalten. Um die Kapselung zu erhalten, wird die Containerstruktur in unserer Bibliotheksdatei .c
und nicht im Header definiert.
Wir müssen Folgendes im Auge behalten:
- Der zugrunde liegende Datenpuffer
- Die maximale Größe des Puffers
- Die aktuelle „Kopf“ -Position (erhöht, wenn Elemente hinzugefügt werden)
- Der aktuelle „Schwanz“ (erhöht, wenn Elemente entfernt werden)
- Ein Flag, das angibt, ob der Puffer voll ist oder nicht
// 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;};
Nachdem unser Container entworfen wurde, können wir die Bibliotheksfunktionen implementieren.
Implementierung
Ein wichtiges Detail ist, dass jede unserer APIs ein initialisiertes Puffer-Handle benötigt. Anstatt unseren Code mit bedingten Anweisungen zu verunreinigen, werden wir Assertions verwenden, um unsere API-Anforderungen im Stil „Design by Contract“ durchzusetzen.
Wenn die Schnittstellen nicht ordnungsgemäß verwendet werden, schlägt das Programm sofort fehl, anstatt dass der Benutzer den Fehlercode überprüfen und behandeln muss.
Zum Beispiel:
circular_buf_reset(NULL);
Erzeugt:
=== C Circular Buffer Check ===Assertion failed: (cbuf), function circular_buf_reset, file ../../circular_buffer.c, line 35.Abort trap: 6
Ein weiterer wichtiger Hinweis ist, dass die unten gezeigte Implementierung nicht threadsicher ist. Der zugrunde liegenden zirkulären Pufferbibliothek wurden keine Sperren hinzugefügt.
Initialisieren und Zurücksetzen
Beginnen wir am Anfang: Initialisieren eines kreisförmigen Puffers. In unserer API stellen Clients den zugrunde liegenden Puffer und die Puffergröße bereit, und wir geben ihnen ein kreisförmiges Pufferhandle zurück. Der Grund, warum wir möchten, dass unsere Benutzer den Puffer bereitstellen, ist, dass dies einen statisch zugewiesenen Puffer ermöglicht. Wenn unsere API den Puffer unter der Haube erstellen würde, müssten wir die dynamische Speicherzuweisung verwenden, die in Programmen für eingebettete Systeme häufig nicht zulässig ist.
Wir müssen eine zirkuläre Pufferstrukturinstanz innerhalb der Bibliothek bereitstellen, damit wir einen Zeiger an den Benutzer zurückgeben können. Ich habe der Einfachheit halber malloc
verwendet. Systeme, die keinen dynamischen Speicher verwenden können, müssen lediglich die Funktion init
ändern, um eine andere Methode zu verwenden, z. B. die Zuweisung aus einem statischen Pool vorab zugewiesener zirkulärer Pufferstrukturen.
Ein anderer Ansatz wäre, die Kapselung zu unterbrechen, sodass Benutzer kreisförmige Puffercontainerstrukturen außerhalb der Bibliothek statisch deklarieren können. In diesem Fall muss circular_buf_init
aktualisiert werden, um einen Strukturzeiger zu erhalten. Wir könnten auch unsere init
-Funktion eine Containerstruktur auf dem Stapel erstellen und zurückgeben. Da die Kapselung jedoch unterbrochen ist, können Benutzer die Struktur ändern, ohne die Bibliotheksroutinen zu verwenden. Wenn Sie die Kapselung beibehalten möchten, müssen Sie mit Zeigern anstelle von konkreten Strukturinstanzen arbeiten.
// 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);
Wir geben ein Handle an eine Struktur zurück, die innerhalb der Bibliothek zugewiesen ist. Sobald wir unseren Container erstellt haben, müssen wir die Werte auffüllen und reset
aufrufen. Bevor wir von init
, stellen wir sicher, dass der Puffercontainer in einem leeren Zustand erstellt wurde.
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;}
Der Zweck der Reset-Funktion besteht darin, den Puffer in einen „leeren“ Zustand zu versetzen, der eine Aktualisierung erfordert head
tail
und full
:
void circular_buf_reset(cbuf_handle_t cbuf){ assert(cbuf); cbuf->head = 0; cbuf->tail = 0; cbuf->full = false;}
Da wir eine Methode zum Erstellen eines kreisförmigen Puffercontainers haben, benötigen wir eine äquivalente Methode zum Zerstören des Containers. In diesem Fall rufen wir free
in unserem Container auf. Wir versuchen nicht, den zugrunde liegenden Puffer freizugeben, da wir ihn nicht besitzen.
void circular_buf_free(cbuf_handle_t cbuf){assert(cbuf);free(cbuf);}
Statusüberprüfungen
Als nächstes implementieren wir die Funktionen, die sich auf den Status des Puffercontainers beziehen.
Die vollständige Funktion ist am einfachsten zu implementieren, da wir ein Flag haben, das den Status darstellt:
bool circular_buf_full(cbuf_handle_t cbuf){assert(cbuf); return cbuf->full;}
Da wir das full
Flag haben, um zwischen vollem oder leerem Zustand zu unterscheiden, kombinieren wir das Flag mit einer Überprüfung, dass head == tail
:
bool circular_buf_empty(cbuf_handle_t cbuf){assert(cbuf); return (!cbuf->full && (cbuf->head == cbuf->tail));}
Die Kapazität unseres Puffers wurde während initialisierung, also geben wir diesen Wert einfach an den Benutzer zurück:
size_t circular_buf_capacity(cbuf_handle_t cbuf){assert(cbuf);return cbuf->max;}
Die Berechnung der Anzahl der Elemente im Puffer war ein schwierigeres Problem als erwartet. Viele vorgeschlagene Größenberechnungen verwenden Modulo, aber ich bin beim Testen auf seltsame Eckfälle gestoßen. Ich habe mich für eine vereinfachte Berechnung mit bedingten Anweisungen entschieden.
Wenn der Puffer voll ist, wissen wir, dass unsere Kapazität maximal ist. Wenn head
größer oder gleich tail
ist, subtrahieren wir einfach die beiden Werte, um unsere Größe zu erhalten. Wenn tail
größer als head
ist, müssen wir die Differenz mit max
, um die richtige Größe zu erhalten.
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;}
Hinzufügen und Entfernen von Daten
Mit den Buchhaltungsfunktionen aus dem Weg, ist es Zeit, in das Fleisch zu graben: Hinzufügen und Entfernen von Daten aus der Warteschlange.
Das Hinzufügen und Entfernen von Daten aus einem kreisförmigen Puffer erfordert die Manipulation der head
und tail
Zeiger. Wenn wir Daten zum Puffer hinzufügen, fügen wir den neuen Wert an der aktuellen head
Position ein, dann gehen wir vor head
. Wenn wir Daten aus dem Puffer entfernen, rufen wir den Wert des aktuellen tail
Zeigers ab und fahren dann tail
.
Das Hinzufügen von Daten zum Puffer erfordert jedoch etwas mehr Nachdenken. Wenn der Puffer full
ist, müssen wir unseren tail
Zeiger sowie head
. Wir müssen auch überprüfen, ob das Einfügen eines Werts die Bedingung full
auslöst.
Wir werden zwei Versionen der put
-Funktion implementieren. Wenn unser Puffer bereits voll ist, gehen wir tail
. Wir bringen head
immer um eins voran. Nachdem der Zeiger fortgeschritten ist, füllen wir das Flag full
, indem wir prüfen, ob head == tail
.
Beachten Sie die Verwendung des Modulo-Operators (%
) unten. Modulo bewirkt, dass die Werte head
und tail
auf 0 zurückgesetzt werden, wenn die maximale Größe erreicht ist. Dadurch wird sichergestellt, dass head
und tail
immer gültige Indizes des zugrunde liegenden Datenpuffers sind.
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);}
Wie Miro Samek hilfreich betonte, ist dies eine teure Rechenoperation. Stattdessen können wir bedingte Logik verwenden, um die Gesamtzahl der Anweisungen zu reduzieren. Miros empfohlener Ansatz ist:
if (++(cbuf->head) == cbuf->max) { cbuf->head = 0;}
Nun wird advance_pointer
so aussehen:
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);}
Wir können eine ähnliche Hilfsfunktion erstellen, die aufgerufen wird, wenn ein Wert aus dem Puffer entfernt wird. Wenn wir einen Wert entfernen, wird das Flag full
auf false
gesetzt und der Endzeiger wird erweitert.
static void retreat_pointer(cbuf_handle_t cbuf){assert(cbuf);cbuf->full = false;if (++(cbuf->tail) == cbuf->max) { cbuf->tail = 0;}}
Wir werden zwei Versionen der put
Funktion erstellen. Die erste Version fügt einen Wert in den Puffer ein und rückt den Zeiger vor. Wenn der Puffer voll ist, wird der älteste Wert überschrieben. Dies ist der Standard-Anwendungsfall für einen kreisförmigen Puffer
void circular_buf_put(cbuf_handle_t cbuf, uint8_t data){assert(cbuf && cbuf->buffer); cbuf->buffer = data; advance_pointer(cbuf);}
Die zweite Version der put
Funktion gibt einen Fehler zurück, wenn der Puffer voll ist. Dies wird zu Demonstrationszwecken bereitgestellt, wir verwenden diese Variante jedoch nicht in unseren Systemen.
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;}
Um Daten aus dem Puffer zu entfernen, greifen wir auf den Wert am tail
zu und aktualisieren dann den tail
Zeiger. Wenn der Puffer leer ist, geben wir keinen Wert zurück oder ändern den Zeiger. Stattdessen geben wir einen Fehler an den Benutzer zurück.
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;}
Damit ist die Implementierung unserer zirkulären Pufferbibliothek abgeschlossen.
Verwendung
Bei Verwendung der Bibliothek ist der Client für die Erstellung des zugrunde liegenden Datenpuffers für circular_buf_init
verantwortlich, und es wird ein cbuf_handle_t
zurückgegeben:
uint8_t * buffer = malloc(EXAMPLE_BUFFER_SIZE * sizeof(uint8_t));cbuf_handle_t cbuf = circular_buf_init(buffer, EXAMPLE_BUFFER_SIZE);
Dieses Handle wird verwendet, um mit allen verbleibenden Bibliotheksfunktionen zu interagieren:
bool full = circular_buf_full(cbuf);bool empty = circular_buf_empty(cbuf);printf("Current buffer size: %zu\n", circular_buf_size(cbuf);
Vergessen Sie nicht, sowohl den zugrunde liegenden Datenpuffer als auch den Container freizugeben, wenn Sie fertig sind:
free(buffer);circular_buf_free(cbuf);
Ein Testprogramm, das die circular buffer Library verwendet, finden Sie im embedded-resources Repository.
Modifikationen zum Entfernen des vollständigen Flags
Wenn Sie das full
Flag entfernen möchten, überprüfen Sie stattdessen, ob das head
eine Position hinter dem Schwanz ist, um festzustellen, ob der Puffer voll ist:
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;}
Wenn wir nun vermeiden Sie die Modulo-Operation, wir können stattdessen bedingte Logik verwenden:
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;}
Der leere Fall ist dann, dass head
und tail
gleich sind:
bool circular_buf_empty(circular_buf_t* cbuf){// We define empty as head == tail return (cbuf->head == cbuf->tail);}
Wenn wir Daten aus dem Puffer erhalten, werden wir den Endzeiger voranbringen und ihn bei Bedarf umwickeln:
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;}
Wenn wir Daten zum Puffer hinzufügen, werden wir die Daten speichern und den Kopfzeiger voranbringen, wenn nötig:
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;}
Andere Verweise auf full
können eliminiert werden.
C++
C++ eignet sich für eine sauberere zirkuläre Pufferimplementierung als C.
Klassendefinition
Wir beginnen mit der Definition unserer C ++ – Klasse. Wir möchten, dass unsere C ++ – Implementierung jede Art von Daten unterstützt, daher werden wir sie zu einer Vorlagenklasse machen.
Unsere APIs werden der C-Implementierung ähnlich sein. Unsere Klasse bietet Schnittstellen für:
- Zurücksetzen des Puffers auf leer
- Hinzufügen von Daten
- Entfernen von Daten
- Überprüfen des vollständigen / leeren Zustands
- Überprüfen der aktuellen Anzahl von Elementen im Puffer
- Überprüfen der Gesamtkapazität des Puffers
Wir werden auch C ++ Smart Pointer verwenden, um sicherzustellen, dass wir keine Daten hinterlassen, sobald unser Puffer zerstört ist. Dies bedeutet, dass wir den Puffer für den Benutzer verwalten können.
Ein weiterer Vorteil von C ++ ist die Trivialität, diese Klasse threadsicher zu machen: Wir können uns auf den std::mutex
Typ verlassen (vorausgesetzt, dies ist für Ihre Plattform definiert).
Hier ist unsere Klassendefinition:
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;};
C ++ – Implementierung
Unser C ++ – Kreispuffer ahmt einen Großteil der Logik der C-Implementierung nach, führt jedoch zu einem viel saubereren und wiederverwendbareren Design. Außerdem verwendet der C ++ – Puffer std::mutex
, um eine threadsichere Implementierung bereitzustellen.
Hinweis: Die gleichen logischen Änderungen können in der C ++ – Implementierung vorgenommen werden, um die Thread-Sicherheit mit einem einzelnen Producer und Consumer zu unterstützen, indem ein Slot „verschwendet“ wird. Weitere Informationen finden Sie in den Anpassungen in der C-Implementierung.
Initialisierung
Beim Erstellen unserer Klasse weisen wir die Daten für unseren zugrunde liegenden Puffer zu und legen die Puffergröße fest. Dadurch entfällt der für die C-Implementierung erforderliche Overhead.
Im Gegensatz zur C-Implementierung ruft der C++ – Konstruktor reset
nicht auf. Da wir Anfangswerte für unsere Mitgliedsvariablen angeben, beginnt unser zirkulärer Puffer im richtigen Zustand.
explicit circular_buffer(size_t size) :buf_(std::unique_ptr<T>(new T)),max_size_(size){//empty constructor}
Unser Reset-Verhalten versetzt den Puffer wieder in einen leeren Zustand (head == tail && !full_
).
void reset(){std::lock_guard<std::mutex> lock(mutex_);head_ = tail_;full_ = false;}
Statusverfolgung
Die Logik der empty
und full
Fälle ist die gleiche wie im C-Beispiel:
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_;}
Im C-Beispiel ++ zirkuläre Pufferimplementierung, size
und capacity
geben die Anzahl der Elemente in der Warteschlange und nicht die Größe in Bytes an. Dies ermöglicht es uns, den zugrunde liegenden Details des Typs gegenüber agnostisch zu sein.
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;}
Daten hinzufügen
Die Logik für put
entspricht der C-Implementierung. Diese Implementierung verwendet das Verhaltensmuster „Ältesten Wert überschreiben“.
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_;}
Hinweis: Der Einfachheit halber habe ich die Option weggelassen, die Modulo-Operationen vermeidet. Sie finden diese Logik im Abschnitt C.
Abrufen von Daten
Die Logik hinter get
entspricht der C-Implementierung. Im Gegensatz zur C-Implementierung wird ein leerer Wert zurückgegeben, wenn der Puffer leer ist.
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;}
Hinweis:
return T()
gibt einen konstruierten Standardwert für den angegebenen Typ zurück. Der tatsächlich erzeugte Wert hängt vom Typ oder vom Konstruktor ab. Der Einfachheit halber habe ich auch die Option ausgelassen, die Modulo-Operationen vermeidet. Sie finden diese Logik im Abschnitt C.
Verwendung
Der C ++ – Kreispuffer ist viel einfacher zu verwenden als die C-Implementierung.
Um einen zirkulären Puffer zu instanziieren, deklarieren wir einfach ein Objekt und geben den Vorlagentyp für unseren Puffer an. Hier ist ein Beispiel mit einem Puffer von 10 uint32_t
Einträgen:
circular_buffer<uint32_t> circle(10);
Das Hinzufügen von Daten ist einfach:
uint32_t x = 100;circle.put(x);
Und das Abrufen von Daten ist ebenso einfach:
x = circle.get()
Denken Sie daran, dass Sie, da dies eine Vorlagenklasse ist, einen kreisförmigen Puffer jedes Typs erstellen können, den Sie benötigen.
Updates für C ++ 17
In C++17 haben wir Zugriff auf std::optional
, wodurch wir einen Wert darstellen können, der vorhanden sein kann oder nicht. Unsere get
Funktion würde eine std::optional<T>
. Wir würden auch std::nullopt
anstelle eines standardmäßig erstellten T
wenn die Warteschlange leer ist.
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;}
Hinweis: Der Einfachheit halber habe ich die Option weggelassen, die Modulo-Operationen vermeidet. Sie finden diese Logik im Abschnitt C.
Im aufrufenden Code können Sie mit dem Booleschen Operator oder der has_value
Memberfunktion nach einem gültigen Wert suchen. Wenn ein gültiger Wert vorhanden ist, kann mit den Operatoren ->
oder *
oder mit der Memberfunktion value()
darauf zugegriffen werden.
// Returns an optionalauto item = cbuf.get();// Check if the optional is validif(item){process_data(*item); // access the value}
Alles zusammenfassen
Beispielimplementierungen finden Sie im embedded-resources
Github-Repository.
- C circular buffer test program
- C circular buffer library
- C++ circular buffer example
Wenn Sie diese Bibliothek erweitern möchten, können Sie zusätzliche APIs hinzufügen, damit Benutzer mehrere Elemente mit einem einzigen Vorgang hinzufügen / entfernen können. Sie können die C-Implementierung auch threadsicher machen.
Thread-Sicherheit mit der Lookahead-Methode
Ein Ansatz für Thread-Sicherheit ohne Mutex ist die „Lookahead“-Methode. Diese Methode unterstützt einen einzelnen Producer-Thread und einen einzelnen Consumer-Thread; mehrere Hersteller oder Verbraucher benötigen eine Sperre.
Anstatt das boolesche Flag zu verwenden, um zwischen den vollen und leeren Fällen zu unterscheiden, lassen wir immer eine Zelle leer. Indem wir eine einzelne leere Zelle verwenden, um den „vollen“ Fall zu erkennen, können wir einen einzelnen Produzenten und einen einzelnen Verbraucher ohne Sperre unterstützen (solange put
und get
nicht die gleichen Variablen ändern).
Sie könnten besorgt sein, einen Slot zu verschwenden, aber dieser Kompromiss ist oft viel billiger als die Kosten für die Verwendung eines OS-Lock-Primitivs.
Weiterführende Literatur
- C ++ Smart Pointer
- Lassen Sie Ihre C-Stye-Zeiger für Smart Pointer fallen
Hier sind andere Implementierungen von zirkulären Puffern:
- QuantumLeaps/lock-free-ring-buffer
- willemt/BipBuffer
Weitere Informationen zu zirkulären Puffern:
- Embedded.com : Ringpuffer Grundlagen
- Wikipedia: Kreispuffer
- Betriebssysteme: Lock Free Ringpuffer
- Boost-Kreispuffer
- C ++: Leistung eines kreisförmigen Puffers im Vergleich zu Vektor, Deque und Liste
- Lock-Free Single-Producer – Single Consumer Circular Queue
Es gibt einen Vorschlag, der C ++ – Standardbibliothek einen kreisförmigen Puffertyp hinzuzufügen:
- P0059: Ein Vorschlag, Ring Span zur Standardbibliothek hinzuzufügen
- Ring Span
- Ring Span Lite
Änderungsprotokoll
- 20210321
- Fehler behoben mit
max_size_
wobeicbuf->max
hätte verwendet werden sollen - Klärender Text hinzugefügt, warum der Puffer vom Benutzer übergeben wird
- Fehler behoben mit
- 20210301
- Adressiertes Feedback von Miro zur Vermeidung von Modulo-Operationen
- 20210213
- Zusätzliche Links hinzugefügt
- Weitere Diskussion über den Kompromiss zwischen vollem Flag und der Verwendung eines „verschwendeten“ änderungen für Thread-Sicherheit mit einem einzelnen Produzenten / Verbraucher anzeigen
- Notizen hinzufügen zu
std::optional
mit C ++ 17 verwenden
- 20191016
- Formatierung des Änderungsprotokollabschnitts für Konsistenz auf der gesamten Site aktualisiert
- Herabgestufte Header für Konsistenz auf der gesamten Site
- Fehlerhafte Inhaltsverzeichnislinks behoben
- Änderungsprotokoll aus Inhaltsverzeichnis entfernt
- 20190627
- Link zum Artikel Lock Free Ring Buffer von Microsoft Systems hinzugefügt.
- 20190604
- Ein Tippfehler wurde behoben (danke Chris Svec!) und einige Formulierungen in Bezug auf den undurchsichtigen Typ geändert.
- 20181219
- Hinweis zur Vermeidung von Parallelitätsproblemen mit einem einzelnen Produzenten und einem einzelnen Verbraucher unter Verwendung eines leeren Steckplatzes hinzugefügt.
- 20180804
- Der Artikel wurde umstrukturiert und neu geschrieben.Vielen Dank an alle, die auf dem Weg Feedback gegeben haben. Die Beispiele wurden aktualisiert, um:
- Defensive Programmierung entfernen
- Assertionen verwenden
- Erstellen Sie eine eigenständige Bibliothek mit einer undurchsichtigen Struktur
- Erweitern Sie die APIs, einschließlich einer Berechnung für die aktuelle kreisförmige Puffergröße
- Aktualisieren Sie die Bibliothek, damit sie keinen Steckplatz verschwendet hat