shared_ptr - use_count nutzen zum Feintuning.



  • 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.


  • Gesperrt

    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.



  • @Helmut-Jakoby

    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 O(1)\mathcal{O}(1)-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 oder std::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#Notes

    Fü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 oder std::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.


  • Gesperrt

    @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 oder std::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 einen std::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 du std::allocate_shared, die "Allocator-Variante" von std::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. mit boost::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 wegen allocator<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 (wie malloc/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 den pmr-Allokatoren deutlich einfacher, weil die diese Funktionalität bereits haben und man nur die memory_resource implementieren muss. Daher dachte ich fälschlicherweise dass das nur mit denen ginge, aber die Motivation für pmr 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 bei rebind weitergereicht wird (und die man mit entsprechendem Aufwand auch mit klassichen Allokatoren implementieren kann)?


Anmelden zum Antworten