2015-07-06 6 views
11

Ich versuche genau zu verstehen, wie thread-sichere, atomare Referenzzählung funktioniert, zum Beispiel wie mit std::shared_ptr. Ich meine, das Grundkonzept ist einfach, aber ich bin wirklich verwirrt darüber, wie das decrf plus delete Rennbedingungen vermeidet.Atomare Referenzzählung

Diese tutorial from Boost demonstriert, wie ein atomares Thread-sicheres Referenzzählsystem mit der Boost-Atombibliothek (oder der C++ 11-Atombibliothek) implementiert werden kann.

Okay, also bekomme ich die allgemeine Idee. Aber ich verstehe nicht, warum das folgende Szenario NICHT möglich ist:

Angenommen, der Refcount ist derzeit 1.

  1. Thread A: atomar decrefs die refcount zu 0.
  2. Thread B: erhöht den Refcount automatisch auf 1.
  3. Thread A: ruft delete auf dem verwalteten Objektzeiger auf.
  4. Thread B: Der Refcount wird als 1 angezeigt, greift auf den Zeiger des verwalteten Objekts zu ... SEGFAULT!

Ich kann nicht verstehen, was das Auftreten dieses Szenario verhindert, da gibt es nichts erreicht die Zeit der refcount 0 ein Daten Rennen von zwischen zu verhindern, und das Objekt gelöscht wird. Das Verringern des Refcounts und das Aufrufen von delete sind zwei separate, nicht atomare Operationen. Wie ist das ohne Schloss möglich?

+5

Wie ist der Refcount 1, während zwei Referenzen von den beiden Threads sind? –

+0

Sie müssen die Invariante einer Verwendungszählung buchstabieren. Eine Verwendungszählung sollte niemals 0 erreichen, solange Benutzer vorhanden sind. Ihr Code muss irgendwo einen Fehler enthalten. – curiousguy

Antwort

15

Wahrscheinlich überschätzen Sie die Threadsicherheit, die shared_ptr bietet.

Das Wesen des Atom ref Zählen ist, um sicherzustellen, wenn zwei verschiedene Instanzen ein shared_ptr (welche das gleiche Objekt verwalten) zugegriffen/verändert, wird es keine Race-Bedingung sein. shared_ptr stellt jedoch die Threadsicherheit nicht sicher, wenn zwei Threads auf dasselbe shared_ptr-Objekt zugreifen (und eines davon ist ein Schreibvorgang). Ein Beispiel wäre z.B. wenn ein Thread den Zeiger dereferenziert, während der andere ihn zurücksetzt.
So über die einzige Sache shared_ptr Garantees ist, dass es keine doppelte Löschung und kein Leck geben wird, solange es keine Rasse auf einer einzelnen Instanz eines shared_ptr gibt (Es macht auch keine Zugriffe auf das Objekt, das es auf threadsafe zeigt)

Als Ergebnis ist auch das Erstellen einer Kopie von shared_ptr nur rennenfrei, wenn es keinen anderen Thread gibt, der sie logisch löschen/zurücksetzen könnte (Sie könnten auch sagen, dass es nicht intern synchronisiert ist) . Dies ist das Szenario, das Sie beschreiben.

Um es noch einmal wiederholen: eine Instanz aus mehreren Threads einzigeshared_ptr Zugriff wobei einer dieser Zugänge ist ein Schreib- (den Zeiger) noch eine Race-Bedingung ist.

Wenn Sie z.B.Kopieren Sie eine std::shared_ptr in einer threadsafe Weise, müssen Sie sicherstellen, dass alle Lasten und speichert passieren std::atomic_... Operationen, die für shared_ptr spezialisiert sind.

+1

Atomare Operationen auf 'shared_ptr' sind wenig bekannt und unterschätzt. +1 – sehe

+0

@sehe: Eine genauere Beschreibung ist, dass "atomare Operationen auf geteilten Zeigern schrecklich falsch entworfen und gebrochen sind" :-) Bemühungen sind im Gange, diese Situation zu verbessern. –

+0

@sehe: Ich muss zugeben, dass ich sie selbst noch nicht benutzt habe. Ich erinnerte mich nur an Herb Sutters Rede über die Lock-Free-Programmierung bei cppcon2014. – MikeMB

3

Ihr Szenario ist nicht möglich, da Thread B bereits mit einem inkrementierten Refcount erstellt worden sein sollte. Thread B sollte die Ref-Zählung nicht als erstes inkrementieren.

Nehmen wir an, Thread A spawnt Thread B. Thread A hat die Aufgabe, die ref count des Objekts zu erhöhen, BEVOR der Thread erstellt wird, um die Threadsicherheit zu gewährleisten. Thread B muss die Freigabe dann nur aufrufen, wenn sie beendet wird.

Wenn Thread A Thread B erstellt, ohne die ref-Zählung zu erhöhen, kann es passieren, dass die beschriebenen Probleme auftreten.

+1

Können Sie einen Link zu der Frage bereitstellen, die Sie beantwortet haben? – IInspectable

+4

@Intspectable: Scheint eine vollkommen vernünftige Antwort auf die Frage auf dieser Seite - oder fehlt mir etwas? – psmears

5

Diese Situation wird nie entstehen. Wenn die Ref-Zählung eines gemeinsam genutzten Zeigers 0 erreicht, wurde die letzte Referenz darauf entfernt, und es ist sicher, den Zeiger zu löschen. Es gibt keine Möglichkeit, einen weiteren Verweis auf den freigegebenen Zeiger zu erstellen, da keine Kopie mehr vorhanden ist.

+0

Ich bin nicht sicher, ob Sie das Szenario nicht erwähnen, weil es sowieso UB ist oder weil Sie es übersehen haben, aber es ist tatsächlich möglich, ein shared_ptr zu kopieren, während das Objekt, auf das es zeigt, gelöscht wird. Z.B. Wenn Sie einen globalen shared_ptr haben und ein Thread eine lokale Kopie davon erstellt, während der andere Thread den Zeiger auf ein anderes Objekt zurücksetzt. Und selbst wenn Sie 'std: share_ptr :: reset' nicht berücksichtigen wollen (ich bin mir nicht sicher, ob intrusive Zeiger das haben), dann denken Sie an eine Situation, in der share_ptr selbst dynamisch zugewiesen wird (sei es direkt oder ein Mitglied eines anderen Objekts) und dann gelöscht. – MikeMB

+0

@MikeMB Das ergibt keinen Sinn. Wenn Sie ein 'shared_ptr' kopieren, das auf ein Objekt zeigt, wurde dieses Objekt nicht gelöscht. Ein 'shared_ptr' löscht ein Objekt erst dann, wenn es bestätigt, dass kein anderer' shared_ptr' darauf zeigt und beim Löschen des Objekts sicherstellt, dass es nicht mehr auf das Objekt zeigt. –

+0

@David: Was Sie sagen, ist nur wahr, wenn Sie über zwei verschiedene Instanzen sprechen. Wenn ein Thread die gleiche Instanz liest (zum Kopieren), in die der andere schreibt (z. B. Aufruf von reset oder Destruktor), haben Sie keine solche Garantie (Datenerfassung -> UB). Das ist zumindest ein Grund, warum sie ein 'std :: atomic_shared_ptr' vorstellen wollen. – MikeMB

0

Thema B: atomar increfs die refcount zu 1.

unmöglich. Um den Referenzzähler auf eins zu erhöhen, müsste der Referenzzähler Null sein. Aber wenn der Referenzzähler Null ist, wie greift Thread B überhaupt auf das Objekt zu?

Entweder Thread B hat einen Verweis auf das Objekt oder nicht. Wenn dies der Fall ist, kann die Referenzzählung nicht null sein. Wenn dies nicht der Fall ist, warum ist es dann mit einem Objekt verwoben, das von intelligenten Zeigern verwaltet wird, wenn es keinen Bezug auf dieses Objekt hat?

3

Die Implementierung stellt keine solche Garantie zur Verfügung oder erfordert diese. Die Vermeidung des von Ihnen beschriebenen Verhaltens beruht auf der ordnungsgemäßen Verwaltung der gezählten Referenzen, die normalerweise über eine RAII-Klasse wie std::shared_ptr erfolgt. Der Schlüssel besteht darin, die Weitergabe von rohen Zeigern über Bereiche hinweg vollständig zu vermeiden. Jede Funktion, die einen Zeiger auf das Objekt speichert oder behält, muss einen gemeinsamen Zeiger verwenden, um die Ref-Zählung richtig zu erhöhen.

void f(shared_ptr p) { 
    x(p); // pass as a shared ptr 
    y(p.get()); // pass raw pointer 
} 

Diese Funktion übergeben wurde ein shared_ptr so die refcount schon war 1+. Unsere lokale Instanz, , sollte ref_count während der Kopierzuweisung gestoßen haben. Als wir x anriefen, wenn wir nach Wert bestanden, erstellten wir einen anderen ref. Wenn wir const ref bestanden, behielten wir unsere aktuelle Ref-Zählung bei. Wenn wir nicht-const ref übergeben, dann ist es möglich, dass x() die Referenz freigegeben und y mit null aufgerufen wird.

Wenn x() den rohen Zeiger speichert/behält, haben wir möglicherweise ein Problem. Wenn unsere Funktion zurückkehrt, könnte der Refcount 0 erreichen und das Objekt könnte zerstört werden. Dies ist unser Fehler, wenn die Ref-Zählung nicht korrekt durchgeführt wird.

Bedenken Sie:

template<typename T> 
void test() 
{ 
    shared_ptr<T> p; 
    { 
     shared_ptr<T> q(new T); // rc:1 
     p = q; // rc:2 
    } // ~q -> rc:1 
    use(p.get()); // valid 
} // ~p -> rc:0 -> delete 

vs

template<typename T> 
void test() 
{ 
    T* p; 
    { 
     shared_ptr<T> q(new T); // rc:1 
     p = q; // rc:1 
    } // ~q -> rc:0 -> delete 
    use(p); // bad: accessing deleted object 
} 
+0

Dies kann jedoch ein Problem sein, indem Sie einfach der Zuweisungsoperator für geteilte Zeiger. Ich kam in eine solche Situation, die ich hier beschrieben habe: http://stackoverflow.com/questions/31918932/what-is-the-cost-of-calling-member-function-via-shared-pointer – philipp

1

Für std::shared_ptr eine Änderung der Referenzzählung ist Thread-sicher, aber nicht den Zugriff auf den Inhalt des `shared_ptr.

In Bezug auf boost::intrusive_ptr<X> ist dies keine Antwort.