Ich habe vor kurzem einen Port zu C++ 11 gemacht, der std :: atomic eines dreifachen Puffers verwendet, um als Gleichzeitigkeits-sync-Mechanismus verwendet zu werden. Die Idee hinter diesem Thread-Sync-Ansatz ist, dass für einen Produzenten-Verbraucher-Situation, wo Sie einen Hersteller haben, der schneller läuft, dass der Verbraucher, Triple-Pufferung einige Vorteile geben kann, da der Produzenten-Thread nicht "verlangsamt" werden wird auf den Verbraucher warten. In meinem Fall habe ich einen Physik-Thread, der bei ~ 120 Bildern pro Sekunde aktualisiert wird, und einen Render-Thread, der bei ~ 60 Bildern pro Sekunde läuft. Offensichtlich möchte ich, dass der Render-Thread immer den neuesten Zustand erhält, aber ich weiß auch, dass ich aufgrund des Unterschieds in den Raten viele Frames aus dem Physik-Thread überspringen werde. Auf der anderen Seite möchte ich, dass mein Physik-Thread seine konstante Aktualisierungsrate beibehält und nicht durch den langsameren Render-Thread eingeschränkt wird, der meine Daten sperrt.C++ 11 atomare Speicherordnung - ist dies eine korrekte Verwendung der entspannten (release-consume) Bestellung?
Der ursprüngliche C-Code wurde von Remis-Gedanken gemacht und die vollständige Erklärung ist in seinem blog. Ich ermutige alle, die daran interessiert sind, es für ein besseres Verständnis der ursprünglichen Implementierung zu lesen.
Meine Implementierung kann here gefunden werden.
Die Grundidee besteht darin, ein Array mit 3 Positionen (Puffer) und einem atomaren Flag zu haben, das compare-and-swapped wird, um zu bestimmen, welche Array-Elemente welchem Zustand zu welchem Zeitpunkt entsprechen. Auf diese Weise wird nur eine atomare Variable verwendet, um alle 3 Indizes des Arrays und die Logik hinter der Dreifachpufferung zu modellieren. Die 3 Positionen des Puffers heißen Dirty, Clean und Snap. Der Producer schreibt immer in den Dirty-Index und kann den Writer umdrehen, um den Dirty mit dem aktuellen Clean-Index zu tauschen. Der Consumer kann einen neuen Snap anfordern, der den aktuellen Snap-Index mit dem Clean-Index tauscht, um den neuesten Puffer zu erhalten. Der Consumer liest immer den Puffer in der Snap-Position.
Die Flagge besteht aus einem 8-Bit unsigned int und die Bits entsprechen:
(ungebraucht) (new write) (2x schmutzig) (2x sauber) (2x Snap)
Die newWrite Das Extra-Bit-Flag wird vom Schreiber gesetzt und vom Leser gelöscht. Der Leser kann dies verwenden, um zu überprüfen, ob seit dem letzten Snap irgendwelche Schreibvorgänge stattgefunden haben, und wenn dies nicht der Fall ist, wird kein weiterer Snap mehr benötigt. Das Flag und die Indizes können unter Verwendung einfacher bitweiser Operationen erhalten werden.
Ok jetzt für den Code:
template <typename T>
class TripleBuffer
{
public:
TripleBuffer<T>();
TripleBuffer<T>(const T& init);
// non-copyable behavior
TripleBuffer<T>(const TripleBuffer<T>&) = delete;
TripleBuffer<T>& operator=(const TripleBuffer<T>&) = delete;
T snap() const; // get the current snap to read
void write(const T newT); // write a new value
bool newSnap(); // swap to the latest value, if any
void flipWriter(); // flip writer positions dirty/clean
T readLast(); // wrapper to read the last available element (newSnap + snap)
void update(T newT); // wrapper to update with a new element (write + flipWriter)
private:
bool isNewWrite(uint_fast8_t flags); // check if the newWrite bit is 1
uint_fast8_t swapSnapWithClean(uint_fast8_t flags); // swap Snap and Clean indexes
uint_fast8_t newWriteSwapCleanWithDirty(uint_fast8_t flags); // set newWrite to 1 and swap Clean and Dirty indexes
// 8 bit flags are (unused) (new write) (2x dirty) (2x clean) (2x snap)
// newWrite = (flags & 0x40)
// dirtyIndex = (flags & 0x30) >> 4
// cleanIndex = (flags & 0xC) >> 2
// snapIndex = (flags & 0x3)
mutable atomic_uint_fast8_t flags;
T buffer[3];
};
Umsetzung:
template <typename T>
TripleBuffer<T>::TripleBuffer(){
T dummy = T();
buffer[0] = dummy;
buffer[1] = dummy;
buffer[2] = dummy;
flags.store(0x6, std::memory_order_relaxed); // initially dirty = 0, clean = 1 and snap = 2
}
template <typename T>
TripleBuffer<T>::TripleBuffer(const T& init){
buffer[0] = init;
buffer[1] = init;
buffer[2] = init;
flags.store(0x6, std::memory_order_relaxed); // initially dirty = 0, clean = 1 and snap = 2
}
template <typename T>
T TripleBuffer<T>::snap() const{
return buffer[flags.load(std::memory_order_consume) & 0x3]; // read snap index
}
template <typename T>
void TripleBuffer<T>::write(const T newT){
buffer[(flags.load(std::memory_order_consume) & 0x30) >> 4] = newT; // write into dirty index
}
template <typename T>
bool TripleBuffer<T>::newSnap(){
uint_fast8_t flagsNow(flags.load(std::memory_order_consume));
do {
if(!isNewWrite(flagsNow)) // nothing new, no need to swap
return false;
} while(!flags.compare_exchange_weak(flagsNow,
swapSnapWithClean(flagsNow),
memory_order_release,
memory_order_consume));
return true;
}
template <typename T>
void TripleBuffer<T>::flipWriter(){
uint_fast8_t flagsNow(flags.load(std::memory_order_consume));
while(!flags.compare_exchange_weak(flagsNow,
newWriteSwapCleanWithDirty(flagsNow),
memory_order_release,
memory_order_consume));
}
template <typename T>
T TripleBuffer<T>::readLast(){
newSnap(); // get most recent value
return snap(); // return it
}
template <typename T>
void TripleBuffer<T>::update(T newT){
write(newT); // write new value
flipWriter(); // change dirty/clean buffer positions for the next update
}
template <typename T>
bool TripleBuffer<T>::isNewWrite(uint_fast8_t flags){
// check if the newWrite bit is 1
return ((flags & 0x40) != 0);
}
template <typename T>
uint_fast8_t TripleBuffer<T>::swapSnapWithClean(uint_fast8_t flags){
// swap snap with clean
return (flags & 0x30) | ((flags & 0x3) << 2) | ((flags & 0xC) >> 2);
}
template <typename T>
uint_fast8_t TripleBuffer<T>::newWriteSwapCleanWithDirty(uint_fast8_t flags){
// set newWrite bit to 1 and swap clean with dirty
return 0x40 | ((flags & 0xC) << 2) | ((flags & 0x30) >> 2) | (flags & 0x3);
}
Wie Sie sehen können, habe ich beschlossen, eine Freigabe-Verbrauchen Muster für Speicherordnung zu verwenden . Die Release (memory_order_release) für den Speicher gewährleistet, dass keine Schreibvorgänge im aktuellen Thread nach dem Speicher neu geordnet werden können. Auf der anderen Seite, die Consume gewährleistet, dass keine Lesevorgänge im aktuellen Thread abhängig von dem Wert derzeit geladen werden kann vor diese Last neu geordnet werden. Dies stellt sicher, dass Schreibvorgänge in abhängigen Variablen in anderen Threads, die dieselbe atomare Variable freigeben, im aktuellen Thread sichtbar sind.
Wenn ich mein Verständnis richtig bin, da die Flags nur atomar gesetzt werden müssen, können Operationen auf anderen Variablen, die die Flags nicht direkt beeinflussen, vom Compiler neu geordnet werden, was mehr Optimierungen ermöglicht. Beim Lesen einiger Dokumente über das neue Speichermodell ist mir auch bewusst, dass diese entspannten Atome nur auf Plattformen wie ARM und POWER (die hauptsächlich wegen ihnen eingeführt wurden) spürbare Auswirkungen haben werden. Da ich ARM anvisiere, glaube ich, dass ich von diesen Operationen profitieren und ein bisschen mehr Leistung herauspressen könnte.
Nun zur Frage:
Bin ich für dieses spezielle Problem richtig in der Release-Verbrauchen entspannt Bestellung mit?
Danke,
André
PS: Sorry für den langen Post, aber ich glaubte, dass einige anständige Kontext für eine bessere Sicht des Problems erforderlich war.
EDIT: Implementiert @ Yakk Vorschläge:
- Fest
flags
weiter lesennewSnap()
undflipWriter()
die direkte Zuordnung verwendet haben, damit Standard mitload(std::memory_order_seq_cst)
. - Die Bit-Fiddling-Operationen wurden zur besseren Übersichtlichkeit in dedizierte Funktionen verschoben.
bool
Rückgabetyp zunewSnap()
hinzugefügt, gibt jetzt false zurück, wenn nichts anderes neu und wahr ist.- Definierte Klasse als nicht kopierbar mit
= delete
Idiom seit beide Kopie und Zuordnung Konstruktoren waren unsicher, wenn dieTripleBuffer
verwendet wurde.
EDIT 2: Fest Beschreibung, die nicht korrekt war (Danke @Useless). Es ist der Consumer, der einen neuen Snap anfordert und aus dem Snap-Index liest (nicht der "Writer"). Entschuldige die Ablenkung und danke an Useless, dass du es aufgezeigt hast.
EDIT 3: die newSnap()
und flipriter()
Funktionen Optimiertes Vorschläge nach @Display Namen, effektiv redundante load()
2 ‚s pro Schleifenzyklus zu entfernen.
einfach ein kurzen Code-Review, keine Antwort hier: Zum Beispiel würde TripleBuffer :: TripleBuffer (const TripleBuffer & t) {' doesn‘ In den meisten Kontexten ist es sicher, wenn der 'TripleBuffer' verwendet wird: Ich wäre versucht, ihn wegzulassen. Gleiches mit 'operator ='. 'newSnap' sollte" false "für" nichts zu lesen "zurückgeben. Sie sollten das ganze bisschen herumfiedeln zu einem einzigen Typ, der ein 'uint8_t & 'nimmt und sinnvolle Werte daraus ausgibt. Sie lesen 'flags' in' newSnap' ohne explizite Speicherbestellgarantien. –
Yakk
newSnap()
sein 'templateWow, danke! Sie haben Recht mit den Kopier- und Zuweisungsoperatoren. Sie sind tatsächlich unsicher, wenn der "TripleBuffer" verwendet wird, da Typ-T-Zuordnungen nicht garantiert atomar sind und der Triple-Puffer, von dem kopiert wird, den Zustand zwischen Aufrufen ändern kann. Sie denken, es ist besser, sie zu entfernen und vom Compiler generieren zu lassen? Oder haben Sie eine Idee, wie Sie es sicher machen können? Ich sehe nicht viele _real_ Verwendungen für sie, wenn der TripleBuffer, von dem kopiert wird, verwendet wird. Ich habe sie der Vollständigkeit halber hinzugefügt, aber vergessen, alle möglichen Situationen zu berücksichtigen. –
Über die 'newSnap'-Funktion kann in der Tat True/False zurückgegeben werden und eine nette Ergänzung sein. Es kann signalisieren, dass wir versuchen, zu schnell zu lesen;) Wirklich tolle Beobachtung über die 'flags' in' newSnap' gelesen! Wie es ist, ist es ein Standard 'load (memory_order_seq_cst)'. Es schadet zwar nicht, aber besiegt meinen ursprünglichen Zweck, Release/Consume-Bestellungen zu verwenden :) Vielen Dank! –