shared_ptr - use_count nutzen zum Feintuning.
-
@wob sagte in shared_ptr - use_count nutzen zum Feintuning.:
@It0101 sagte in shared_ptr - use_count nutzen zum Feintuning.:
Der ReferenceCounter sollte prinzipiell threadsafe sein. Mit meinem geplanten Vorgehen würde ich auch nur lesend auf den Inhalt des shared_ptr zugreifen, wenn ich jetzt keinen Denkfehler habe.
Du hast aber doch geschrieben:
Ist er [der refcount] gleich 1, kann ich das Objekt im vorhandenen shared_ptr updaten, denn der Storage ist der einzige Nutzer des Objekts.
Updaten ist nicht "nur lesen". Du checkst, dass der count 1 ist. Ist er. Dann kann ich ja schnell - moment, anderer Thread arbeitet, refcount ist jetzt 2 - updaten. Boom.
Es sind zwei Threads.
Einer betreibt den Storage. Einer den Client.
Im Storage kommen Updates rein und auch die Anfrage aus dem Clientthread.Bei einem Update wird der refcount geprüft. Wenn er größer 1 ist, bedeutet das, dass irgendein Clientthread noch eine kopie vom shared_ptr in der Hand hat. D.h. ich darf den shared_ptr nicht verändern, aber lesen.
Das nutze ich um eine Kopie vom Inhalt des shared_ptr in einem neuen shared_ptr anzulegen und direkt das Update einzuspielen und dann den bestehenden shared_ptr im Storage zu ersetzen.Der shared_ptr den sich der Client in dem Moment ausgeliehen hat, geht dann im refcount von 2 auf 1. Da der RefCount aber threadsafe sein sollte, sollte das kein Problem sein.
-
@It0101 Der Punkt von @wob ist, was passiert, wenn refcount == 1. Dann möchtest du direkt updaten.Wenn jetzt aber nach der Überprüfung auf refcount der Client kommt um sich eine Kopie des shared_ptr zu holen, befindet sich der gerade eigentlich im Update und du möchtest wahrscheinlich nicht, dass der Client damit arbeitet.
-
Würde es ausreichen, die Abfrage des refcount und das ggf. folgende update mit einem mutex abzusichern?
-
@Helmut-Jakoby sagte in shared_ptr - use_count nutzen zum Feintuning.:
Würde es ausreichen, die Abfrage des refcount und das ggf. folgende update mit einem mutex abzusichern?
Glaube ich fast nicht.
Annahme Thread A ist gerade am versenden eines Objekts und wird unterbrochen. Thread B kommt danach an die Reihe und akualisiert just in diesem Moment das zu versendete Obekt.
-
@Quiche-Lorraine
Stellt sich dann die Frage, warum nicht nur mit einem Objekte arbeiten. Warum eine Kopie fürs updaten erstellen und dann das Objekt im Storage ersetzen? Man könnte ja einem "Writer" fürs updaten und ggf. auch mehrere "Reader" zum lesen etablieren. Wenn der "Writer" updaten möchte, müssen halt die Reader warten.
-
Du riskierst durch fehlende "Synchronisierung bzw. MutEx" (weiß nicht, wie man das in C++ nennt) data loss und race conditions.
Da https://www.techtarget.com/searchstorage/definition/race-condition:
Read-modify-write. This kind of race condition happens when two processes read a value in a program and write back a new value. It often causes a software bug. Like the example above, the expectation is that the two processes will happen sequentially -- the first process produces its value and then the second process reads that value and returns a different one.
Lesen-Ändern-Schreiben. Diese Art von Race Condition tritt auf, wenn zwei Prozesse einen Wert in einem Programm lesen und einen neuen Wert zurückschreiben. Dies verursacht oft einen Softwarefehler. Wie im obigen Beispiel wird erwartet, dass die beiden Prozesse nacheinander ablaufen - der erste Prozess erzeugt seinen Wert und der zweite Prozess liest diesen Wert und gibt einen anderen zurück.
Beispiel:
Oma will 100 Euro abheben.
Gleichzeitig will die Bank 150 zwecks Lastschrift abbuchen.
Oma liest 1000 Euro, verringert um 100 Euro und schreibt 900 zurück.
Bank liest 1000 Euro, verringert um 150 Euro und schreibt 850 zurück.
=> Verlust von 100 Euro für die Bank.Es ist aber auch möglich, dass ein "ungültiger" bzw. ein nur halb aktualisierter Wert gelesen wird. In einem solchen Fall ist's wie Lotto.
-
@Helmut-Jakoby sagte in shared_ptr - use_count nutzen zum Feintuning.:
Stellt sich dann die Frage, warum nicht nur mit einem Objekte arbeiten. Warum eine Kopie fürs updaten erstellen und dann das Objekt im Storage ersetzen? Man könnte ja einem "Writer" fürs updaten und ggf. auch mehrere "Reader" zum lesen etablieren. Wenn der "Writer" updaten möchte, müssen halt die Reader warten.
Die Kopie ist aber auch ein Synchonisationselement. Denn der Thread B aktualisiert ja die Objekte, erstellt die Kopie und füttert damit den Versende-Thread A. Und egal was nun passiert, Thread B verändert die Kopie nicht mehr.
-
Oder mal ein anderes Problem.
Nehmen wir mal an, es würde keine Kopie, sondern nur eine Referenz benutzt werden. Und es würde kein
std::shared_ptr
benutzt werden. Dann kann folgendes passieren:- Thread A ist gerade am Senden eines Objekts,
- Thread B aktualisiert die Objekte und fügt dieses dem Storage (std::vector) hinzu.
- Da der Storage intern voll ist, allokiert dieser intern neuen Speicher, kopiert die alten Werte hinein und löscht den alten Speicher
- Dadurch wird die Referenz ungültig s.d. der Thread A auf einmal Unsinn versendet.
-
@It0101 Das klingt ein wenig, als wolltest du mit dieser Aktion vor allem eine Allokation einsparen, wenn du das Objekt sicher wiederverwenden kannst.
Vielleicht macht es da alternativ mehr Sinn, bei eben diesen Allokationen anzusetzen. Du könntest z.B. mit einem Memory Pool arbeiten. Der verwaltet Speicherblöcke, die alle die selbe Größe haben und ist in der lage, sehr schnelle -Allokationen durchzuführen (einfach nächsten freien Block aus der "free list" zurückgeben).
Ab C++17 könntest du mal schauen, ob da vielleicht z.B. mit
std::pmr::unsynchronized_pool_resource
oderstd::pmr::synchronized_pool_resource
was geht. "synchronized" ist threadsicher, wahrscheinlich willst du das, es sei denn Allokationen und Deallokationen finden immer im selben Thread statt - "unsynchronized" hat da wahrscheinlich ein bisschen weniger Overhead. Ich selbst hab die noch nicht verwendet, aber ich würde das mal zuerst ausprobieren. So ein Pool hätte auch den Vorteil, dass du nicht nur Allokationen sparst, wenn es keine Kollisionen mit anderen Threads gibt, sondern das jede Allokation recht preisgünstig ist.
-
@It0101 sagte in shared_ptr - use_count nutzen zum Feintuning.:
Der ReferenceCounter sollte prinzipiell threadsafe sein.
Ja, ist er auch. Er ist auch effizient auszulesen. Bloss gibt dir
use_count
leider nicht die ausreichenden Garantien. Siehe https://en.cppreference.com/w/cpp/memory/shared_ptr/use_count#NotesFür micht klingt das nach einem guten Anwendungsfall für einen
boost::intrusive_ptr
. Da kannst du selbst bestimmen welche Garantien dir der Use-Count gibt und welche nicht.
-
@Schlangenmensch sagte in shared_ptr - use_count nutzen zum Feintuning.:
@It0101 Der Punkt von @wob ist, was passiert, wenn refcount == 1. Dann möchtest du direkt updaten.Wenn jetzt aber nach der Überprüfung auf refcount der Client kommt um sich eine Kopie des shared_ptr zu holen, befindet sich der gerade eigentlich im Update und du möchtest wahrscheinlich nicht, dass der Client damit arbeitet.
Der Fall kann nicht auftreten, da alles über eine FIFO-Queue reinkommt. Sowohl die Updates, als auch die Requests, die auch immer sofort abgearbeitet werden.
-
@Finnegan sagte in shared_ptr - use_count nutzen zum Feintuning.:
@It0101 Das klingt ein wenig, als wolltest du mit dieser Aktion vor allem eine Allokation einsparen, wenn du das Objekt sicher wiederverwenden kannst.
Genau das und auch den Aufwand des Kopierens. Der MemoryPool ist keine üble Idee. Es wäre dann allerdings ein synced. den könnte ich auch für die Updates verwenden, denn im Grunde ist es immer die selbe Klasse, die im ganzen Backend verwendet wird. Sowohl für den Transport der Updates als auch für die Antworten an die Clients.
std::pmr::unsynchronized_pool_resource
oderstd::pmr::synchronized_pool_resource
Sehe ich mir an. Noch nie zuvor davon gehört.
-
Schön, das hier so eine lebhafte Diskussion entstanden ist. Danke zunächst mal für alle eure Beiträge
Mein Anwendungsfall kann ja eigentlich gar nicht so selten sein. Das Füttern eines Storage und das extrahieren von Daten daraus, ist doch eigentlich ein weitverbreiteter Fall.
-
@It0101 sagte in shared_ptr - use_count nutzen zum Feintuning.:
ist doch eigentlich ein weitverbreiteter Fall
Ja, nur das hier noch die Netzwerkkomponente (verteilte Anwendung) und das Multi-Threading (Parallelität/Nebenläufigkeit) hinzukommen. Aber schlussendlich ist es wie Fahrradfahren oder ein Bild malen... wenn man das Handwerk einmal beherrscht, kann man es.
-
@It0101 sagte in shared_ptr - use_count nutzen zum Feintuning.:
@Finnegan sagte in shared_ptr - use_count nutzen zum Feintuning.:
@It0101 Das klingt ein wenig, als wolltest du mit dieser Aktion vor allem eine Allokation einsparen, wenn du das Objekt sicher wiederverwenden kannst.
Genau das und auch den Aufwand des Kopierens. Der MemoryPool ist keine üble Idee. Es wäre dann allerdings ein synced. den könnte ich auch für die Updates verwenden, denn im Grunde ist es immer die selbe Klasse, die im ganzen Backend verwendet wird. Sowohl für den Transport der Updates als auch für die Antworten an die Clients.
Ja, das eigentliche "Kopieren" ohne die Allokation musst du ja auch dann machen, wenn du ein bereits vorhandenes Objekt wieder verwendest und "updatest", wie du in deinem ersten Beitrag geschrieben hast.
std::pmr::unsynchronized_pool_resource
oderstd::pmr::synchronized_pool_resource
Sehe ich mir an. Noch nie zuvor davon gehört.
Was mir bei der Doku zu diesen "pool"-Ressourcen auffällt ist, dass dort nicht wirklich Garantien zur Effizienz der Allokationen gegeben werden und dass diese auch mehrere verschiedene Blockgrößen handhaben können.
Daher mehren sich gerade meine Zweifel, ob die tatsächlich in der lage sind, den generischen Allocator zu schlagen. Immerhin müssen die auch erstmal in ihrer internen Datenstruktur die richtige Blockgröße bestimmen und wo sie den Speicher für Allokationen dieser Größe her holen. Das klingt ein klein wenig aufwändiger als einfach nur stumpf den ersten freien Block aus einer einzigen "Free List" zurückzugeben. Der generische Allocator dürfte das ähnlich handhaben und ist im Allgemeinen bereits exzellent optimiert.
Wahrscheinlich fährst du was Performance angeht mit einem dedizierten Memory Pool für nur eine einzige Blockgröße besser, z.B. sowas wie Boost.Pool.
Auch brauchst wohl du nicht unbedingt einen polymorphen Allocator wie die in
std::pmr
(der Hauptgrund für die ist, dass sie zustandsbehaftet sind und pro Instanz eigenen Speicher verwalten können, während die klassischen Allokatoren "global" arbeiten und nur "pro Allocator-Typ" individuellen Speicher haben können). Ein nicht-pmr
-Allocator tuts denke ich genau so und käme ohne den (winzigen) Overhead einer polymorphen Klasse aus.Auch empfehle ich wie @hustbaer ebenfalls
boost::intrusive_ptr
, allerdings aus etwas anderen Gründen: Wenn du einenstd::shared_pointer
erzeugst, ist dafür nicht nur eine Allokation für das Objekt selbst, sondern auch für den Kontrollblock notwendig, der den Referenzzähler enthält. Es werden also entweder zwei Allokationen mit verschiedenen Größen benötigt, oder - falls dustd::allocate_shared
, die "Allocator-Variante" vonstd::make_shared
verwendest - eine Allokation unbekannter Größe, da der Kontrollblock ein Implementations-Detail der Standardbibliothek ist (auch wenn der meist wahrscheinlich nur ein 32/24-bit Integer + eventuelles Padding für Alignment des Objekts sein dürfte). Du kannst also nicht exakt vorhersagen wie groß die Speicherblöcke des Memory Pool sein müssen. mitboost::intrusive_ptr
hättest du das Problem nicht, da dort der Referenzzähler Teil des Objekts ist.Und natürlich wie immer wenns um Performance geht: messen. Aber das sollte dir klar sein. Durchaus möglich, dass die Performance-Ausbeute gemessen an dem Aufwand nur ziemlich mager ausfällt. Aber zumindest theoretisch ist das Potential da, falls die Allokationen bei dem Code tatsächlich dominant sind.
-
@Finnegan sagte in shared_ptr - use_count nutzen zum Feintuning.:
Auch brauchst wohl du nicht unbedingt einen polymorphen Allocator wie die in
std::pmr
(der Hauptgrund für die ist, dass sie zustandsbehaftet sind und pro Instanz eigenen Speicher verwalten können, während die klassischen Allokatoren "global" arbeiten und nur "pro Allocator-Typ" individuellen Speicher haben können). Ein nicht-pmr
-Allocator tuts denke ich genau so und käme ohne den (winzigen) Overhead einer polymorphen Klasse aus.Da bin ich jetzt nicht bei Dir. Auch die „klassischen“ nicht polymorphen Allocatoren können einen Zustand haben und eigenen Speicher pro Instanz verwalten. Der Punkt ist, dass man das über die entsprechenden alloc_traits vorher testen muss, wie sich ein Allokator verhält.
Ein polymorpher Allokator verhält sich pro Instanz unterschiedlich, d.h. er kann nicht nur unterschiedliche Speicherpools verwalten, er kann für jede Instanz eine eigene Strategie verwenden.
-
@john-0 sagte in shared_ptr - use_count nutzen zum Feintuning.:
@Finnegan sagte in shared_ptr - use_count nutzen zum Feintuning.:
Auch brauchst wohl du nicht unbedingt einen polymorphen Allocator wie die in
std::pmr
(der Hauptgrund für die ist, dass sie zustandsbehaftet sind und pro Instanz eigenen Speicher verwalten können, während die klassischen Allokatoren "global" arbeiten und nur "pro Allocator-Typ" individuellen Speicher haben können). Ein nicht-pmr
-Allocator tuts denke ich genau so und käme ohne den (winzigen) Overhead einer polymorphen Klasse aus.Da bin ich jetzt nicht bei Dir. Auch die „klassischen“ nicht polymorphen Allocatoren können einen Zustand haben und eigenen Speicher pro Instanz verwalten. Der Punkt ist, dass man das über die entsprechenden alloc_traits vorher testen muss, wie sich ein Allokator verhält.
Ein polymorpher Allokator verhält sich pro Instanz unterschiedlich, d.h. er kann nicht nur unterschiedliche Speicherpools verwalten, er kann für jede Instanz eine eigene Strategie verwenden.
Ja, das stimmt. Mein Gedanke war, dass die klassischen Allokatoren keine
memory_resource
haben, Kopien des Allocator untereinander austauschbar sein müssen (Speicher, der mit einem Allocator geholt wird, muss mit der Kopie freigebbar sein) und das wegenallocator<T>::typename rebind<U>::other
auch noch über Typgrenzen hinweg funktionieren muss. Ich hatte das mental so abgespeichert, dass das nur sinnvoll mit einer gobalen Speicher-Ressource geht (wiemalloc
/free
für den Default-Allocator).Aber natürlich kann man auch einen eigenen Allocator implementieren, der ebenfalls so eine Abstraktion wie
memory_resource
verwendet, die dann zwischen den Kopien weitergereicht wird. Allerdings ist das mit denpmr
-Allokatoren deutlich einfacher, weil die diese Funktionalität bereits haben und man nur diememory_resource
implementieren muss. Daher dachte ich fälschlicherweise dass das nur mit denen ginge, aber die Motivation fürpmr
war wohl vornehmlich sowas hier zu vereinfachen:std::vector<int, A1> v1; std::vector<int, A2> v2; v1 = v2; // Inkompatible Typen
Habe ich das richtig verstanden? Die
pmr
-Allokatoren und Container lösen eigentlich 2 Probleme? Einmal die Kompatibilität von Datenstrukturen, die unterschiedliche Allokatoren verwenden und zum anderen die der gemeinsamen Speicher-Ressource die auch beim Kopieren und beirebind
weitergereicht wird (und die man mit entsprechendem Aufwand auch mit klassichen Allokatoren implementieren kann)?