Generische Ringspeicher
Grijjy, Inc. Consulting stellt auf GitHub verschiedene Bibliotheken, Komponenten und Quellcode von Klassen, die in ihrer Anwendungsentwicklung zum Einsatz kommen, zur Verfügung. Ein Teil davon bezieht sich auf generische Ringspeicher, die in dem folgenden Beitrag beschrieben werden:
„Was ist ein Ringspeicher?
Ein Ringspeicher ist ein Puffer mit einer festen Größe der am Anfang und Ende miteinander verbunden ist. Er ähnelt einer Queue mit seinem Verhalten eines FIFO-Puffers (First-In-First-Out). Anders als eine Queue wächst er allerdings nicht, wenn der Puffer voll ist. Das macht Ringspeicher sehr schnell und Speichereffizient.
Ringspeicher sind ideal für das Puffern von Daten, die ungleichmäßig empfangen werden, aber gleichmäßig weitergegeben werden müssen. Sie können das jedes Mal beim Anschauen oder -hören von Streamingmedien in Aktion sehen: Der Media Player Puffert ein paar Millisekunden bis Sekunden an eingehenden Daten, um Sie mit einer gleichbleibenden Rate wiedergeben zu können. In Medienanwendungen wird ein solcher Puffer auch als Jitterbuffer bezeichnet, da er den Jitter kompensiert.
Wir benutzen einen Ringspeicher in unserer grijjy-Anwendung um Audio für die Wiedergabe und Stimmaufzeichnung zu puffern.
Ein Beispiel für einen Ringspeicher kann so aussehen:
Dieser Ringspeicher hat eine feste Größe von 16 Elementen (Buchstaben in diesem Fall). Wir haben 11 Zeichen (A – K) eingetragen, also bleiben noch 5 übrig.
Wir lesen von dem Ringspeicher beim Lesezeiger. Wenn wir beispielsweise 2 Elemente lesen, erhalten wir die Zeichen A und B und rücken den Lesezeiger um 2 vor:
Der Ringspeicher hat jetzt Platz für 7 weitere Elemente, die wir beim Schreibzeiger speichern können. Nach 5 weiteren Elementen beginnen wir damit, die Plätze zu überschreiben, die von den Zeichen A und B eingenommen wurden. Speichern wir beispielsweise 6 weitere Zeichen, wird der Puffer so aussehen:
Die vorherige A-Position wurde mit einem Q überschrieben und es ist immer noch ein Element vorhanden, das vormals den Buchstaben B enthielt.
Ein generischer Ringspeicher
Ringspeicher können nützlich für das Speichern von Daten verschiedener Typen sein. Meistens findet man Ringspeicher, die lediglich generische Daten als Bytepuffer speichern. Aber in beispielsweise Audioanwendungen ist es sinnvoller Audio Samples zu puffern, die üblicherweise als 16-bit-Integer (Int16 oder SmallInt in Delphi) oder als single-precision floating-point (Single in Delphi) gespeichert werden. Wenn Audio in verzahntem Stereoformat gespeichert wird, wird jedes Sample als ein Record mit zwei Werten dargestellt (Für den linken und rechten Kanal).
Wieso also nicht einen generischen Ringspeicher erstellen, der mit verschiedenen Typen genutzt werden kann? Wir haben einen namens TgoRingBuffer<T> nur für Sie erstellt. Sie können ihn in der Unit Grijjy.Collections in der GrijjyFoundation-Repository finden.
Den Ringspeicher benutzen
Das API für den Ringspeicher ist recht einfach:
type |
Der Ringspeicher wird durch das Übergeben der Capacity an den Konstruktor erstellt. Das ist die Anzahl an Elementen (Des Typs T), die der Ringspeicher aufnehmen kann. Der Ringspeicher wird niemals über diese Kapazität hinauswachsen.
Die Kapazität kann durch die Capacity-Property abgerufen werden. Die Count-Property gibt die Anzahl an Elementen zurück, die aktuell in dem Puffer (verfügbar zum lesen) sind. Und schließlich eine Available-Property, die die Anzahl von verfügbaren (freien) Items im Ringspeicher (verfügbar zum schreiben) ausgibt. Alle diese Properties geben die Anzahl von Elementen (des Typs T), nicht die Anzahl der Bytes.
Der Rest des API besteht aus Lese- und Schreibmethoden in verschiedenen Variationen:
- Jede Methode gibt es in zwei Variationen: Eine, in der die Daten als dynamisches Array (TArray<T>) und eine, in der die Daten als offenes Array (array of T) weitergegeben werden. Das erlaubt mehr Flexibilität bezüglich ihrer Nutzung. Wir haben all diese Variationen nicht gezeigt, um es kurz zu halten.
- Die Read-Methoden erzeugen kein Array. Anstelle dessen wird ein vorab zugewiesenes Array (das statisch oder dynamisch sein kann) weitergegeben.
- Es gibt außerdem reguläre Read- und Write-Methoden und TryRead- und TryWrite-Variationen. Die regulären Versionen versuchen die vorgegebene Anzahl an Elementen zu lesen (oder schreiben), können aber weniger lesen oder schreiben, wenn nicht genug Elemente (oder freier Platz) vorhanden ist. Diese Methoden geben die tatsächliche Anzahl an Elementen wieder, die gelesen (oder geschrieben) werden. Die Try*-Versionen versuchen entweder, die vorgegebene Menge zu lesen (oder schreiben), oder fehlschlagen, wenn nicht genügend Elemente (oder freier Platz) verfügbar sind. Die gesamte Operation ist entweder erfolgreich, oder schlägt fehl (wie dem zurückgegebenem Boolean-Wert entnehmbar).
- Und schließlich gibt es für jede Methode eine Überladung, die nur ein Segment des Arrays liest (oder beschreibt). Hier ist der AIndex der Ausgangsindex in das Array und ACount spezifiziert die Anzahl an zu lesenden (oder schreibenden) Elementen, ausgehend von AIndex.
Beachten Sie, dass anders als andere (generische) Sammlungen wie Listen, Stacks oder Queues, hier nicht mit einzelnen Werten gearbeitet wird. Stattdessen wird ein Array von Werten gelesen oder geschrieben. Das hebt hervor, dass diese Klasse normalerweise für Streaming- oder Pufferanwendungen genutzt wird.
Beispielnutzung
Sagen wir mal, wir wollen das obige Beispiel nachbilden. Wir müssen dazu einen Ringspeicher mit 16 Elementen des Typs Char erstellen und 11 Buchstaben (A bis K) hineinschreiben:
var RingBuffer: TgoRingBuffer<Char>; begin RingBuffer := TgoRingBuffer<Char>.Create(16); RingBuffer.Write(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']); ... |
Das Ergebnis ist das zuerst gezeigte Bild:
Wir können dies auch als ein „flaches“ Array darstellen (Was eigentlich der Weg ist, auf dem der Ringspeicher implementiert ist):
Wir lesen die ersten beiden Elemente zu einem statischen Array, ausgehend von Position 0 des Arrays.
{ Reserve some space to read data from the ring buffer: } var ReadBuffer: array [0..9] of Char; Count: Integer; { Read data } Count := RingBuffer.Read(ReadBuffer, 0, 2); Assert(Count = 2); |
Jetzt sind 7 Positionen verfügbar: 5 leere und 2, die zuvor durch A und B belegt waren. Also wird das Schreiben von 6 weiteren Elementen in folgendem resultieren:
Count := RingBuffer.Write(['L', 'M', 'N', 'O', 'P', 'Q']); Assert(Count = 6); |
Wenn wir jetzt versuchen, 3 Elemente zu schreiben, wird nur 1 passen:
Count := RingBuffer.Write(['R', 'S', 'T']); Assert(Count = 1); |
An dieser Stelle ist der Ringspeicher voll und kann nicht weiter beschrieben werden, bis Daten gelesen werden. Es können nun bis zu 16 Elemente (C bis R) gelesen werden.
Anwendungshinweise:
Wie Sie vielleicht in der Record Constraint der Klassendeklaration gesehen haben, kann der Ringspeicher nur mit Wertetypen (Wie Integer, Character, Floating-Point, Enums und Records) genutzt werden:
type TgoRingBuffer<T: record> = class ... end; |
Es ist nicht möglich, den Ringspeicher mit Referenztypen (Wie Strings und Objekte) zu verwenden.
Das ergibt Sinn, denn ein Ringspeicher wird zur Pufferung von Daten und nicht Objekten genutzt. Mit dieser Einschränkung können wir die Implementierung der Klasse optimieren und vereinfachen, da wir Referenztypen nicht berücksichtigen müssen.
Jetzt denken Sie vielleicht: „Na gut, dann verpacke ich einen Referenztyp einfach in einem Record“, wie hier:
type TWrappedString = record Value: String; end; var RingBuffer: TgoRingBuffer; |
Während das zwar kompiliert wird, wird eine Assertion in der Laufzeit ausgelöst, weil wir im Konstruktor prüfen, ob der Typ ein verwalteter Typ ist:
constructor TgoRingBuffer.Create(const ACapacity: Integer); begin Assert(not IsManagedType(T)); inherited Create; ... end; |
Die magische Kompilerfunktion IsManagedType gibt True zurück, wenn der gegebene Typ ein Speicherverwalteter Typ wie String oder Objekt ist. Sie gibt auch dann True zurück, wenn der Typ selbst nicht verwaltet ist (wie Records), aber einen verwalteten Typ enthält (Wie das String-Feld in dem obigen Beispiel).
Auf nicht-ARC-Plattformen werden Sie dennoch in der Lage sein, diesen „Hack“ zu benutzen, um ein Objekt zu verpacken. Das wird allerdings nicht auf ARC-Plattformen funktionieren, da diese auch Objekte verwalten.“
Nach dem Originalpost „Expand your Collections collection – Part 2: a generic ring buffer“, von Erik van Bilsen auf http://www.grijjy.com/, 12.01.2017. Übersetzung von Nils Eilers. Alle Bilder in der Übersetzung stammen ebenfalls aus dem Post „Expand your Collections collection – Part 2: a generic ring buffer“.