Die Geschichte des gefährlichen std :: enable_shared_from_this oder das Zombie-Antimuster

Der Artikel beschreibt das gefährliche Antimuster "Zombies", das in einigen Situationen natürlich auftritt, wenn std :: enable_shared_from_this verwendet wird. Das Material befindet sich irgendwo an der Schnittstelle zwischen moderner C ++ - Technologie und Architektur.

Einführung


C ++ 11 stellte dem Entwickler wunderbare Tools für die Arbeit mit Speicher zur Verfügung - intelligente Zeiger std :: unique_ptr und eine Reihe von std :: shared_ptr + std :: schwach_ptr. Die Verwendung von intelligenten Zeigern für Bequemlichkeit und Sicherheit überwiegt bei weitem die Verwendung von Rohzeigern. Intelligente Zeiger sind in der Praxis weit verbreitet Ermöglichen Sie dem Entwickler, sich auf übergeordnete Probleme zu konzentrieren, als die Richtigkeit der Erstellung / Löschung dynamisch erstellter Entitäten zu verfolgen.
Die Klassenvorlage std :: enable_shared_from_this ist ebenfalls Teil des Standards, und es scheint ziemlich seltsam, wenn Sie sie zum ersten Mal treffen.
In dem Artikel wird erläutert, wie Sie bei der Verwendung hängen bleiben können.

Bildungsprogramm


RAII und Smart Pointer
Der direkte Zweck von intelligenten Zeigern besteht darin, sich um ein Stück RAM zu kümmern, das auf dem Heap zugewiesen ist. Intelligente Zeiger implementieren das RAII-Idiom (Ressourcenerfassung ist Initialisierung) und können leicht angepasst werden, um andere Arten von Ressourcen zu verwalten, die eine Initialisierung und eine nicht triviale De-Initialisierung erfordern, wie z.
- Dateien;
- temporäre Ordner auf der Festplatte;
- Netzwerkverbindungen (http, Websockets);
- Ausführungsthreads (Threads);
- Mutexe;
- andere (was für die Fantasie ausreicht).
Für eine solche Verallgemeinerung reicht es aus, eine Klasse zu schreiben (manchmal kann man sogar keine Klasse schreiben, sondern nur Deleter verwenden - aber heute geht es in der Geschichte nicht darum), was Folgendes implementiert:
- Initialisierung im Konstruktor oder in einer separaten Methode;
- Deinitialisierung im Destruktor,
Wickeln Sie es dann in den entsprechenden Smart Pointer ein, abhängig vom erforderlichen Eigentumsmodell - Joint (std :: shared_ptr) oder Sole (std :: unique_ptr). Dies führt zu einem „zweischichtigen RAII“: Mit einem intelligenten Zeiger können Sie das Eigentum an der Ressource übertragen / teilen, und die Benutzerklasse initialisiert / de-initialisiert eine nicht standardmäßige Ressource.
std :: shared_ptr verwendet einen Linkzählmechanismus. Der Standard definiert den Zähler für starke Links (zählt die Anzahl der vorhandenen Kopien von std :: shared_ptr) und den Zähler für schwache Links (zählt die Anzahl der vorhandenen Kopien von std :: schwach_ptr, die für diese Instanz von std :: shared_ptr erstellt wurden). Das Vorhandensein mindestens einer starken Verbindung stellt sicher, dass die Zerstörung noch nicht erfolgt ist. Diese std :: shared_ptr-Eigenschaft wird häufig verwendet, um die Gültigkeit eines Objekts sicherzustellen, bis die Arbeit damit in allen Teilen des Programms abgeschlossen ist. Das Vorhandensein eines schwachen Glieds verhindert nicht die Zerstörung des Objekts und ermöglicht es Ihnen, ein starkes Glied nur zu erhalten, bis es zerstört wird.
RAII garantiert, dass die Freigabe einer Ressource viel zuverlässiger ist als ein expliziter Aufruf zum Löschen / Löschen [] / frei / schließen / zurücksetzen / entsperren, weil:
- Sie können den expliziten Anruf einfach vergessen;
- Ein expliziter Anruf kann fälschlicherweise mehrmals getätigt werden.
- Eine explizite Herausforderung ist schwierig, wenn das gemeinsame Eigentum an einer Ressource implementiert wird.
- Der Stack-Promotion-Mechanismus in c ++ garantiert den Aufruf von Destruktoren für alle Objekte, die im Ausnahmefall den Gültigkeitsbereich verlassen.
Die Garantie der De-Initialisierung im Idiom ist so wichtig, dass sie neben der Initialisierung einen guten Platz im Namen des Idioms verdient.
Intelligente Zeiger haben auch Nachteile:
- das Vorhandensein von Overhead in Bezug auf Leistung und Speicher (für die meisten Anwendungen ist dies nicht signifikant);
- die Möglichkeit, dass zyklische Verbindungen die Freisetzung der Ressource blockieren und zu deren Leck führen.
Sicherlich hat jeder Entwickler mehr als einmal über zirkuläre Links gelesen und synthetische Beispiele für problematischen Code gesehen.
Die Gefahr kann aus folgenden Gründen unbedeutend erscheinen:
- Wenn der Speicher häufig und häufig leckt - dies macht sich im Verbrauch bemerkbar und wenn selten und wenig -, ist es unwahrscheinlich, dass das Problem auf der Ebene des Endbenutzers auftritt.
- verwendet eine dynamische Code-Analyse für Lecks (Valgrind, Clang LeakSanitizer usw.);
- "Ich schreibe nicht so";
- "meine Architektur ist korrekt";
"Unser Code wird überprüft."

std :: enable_shared_from_this
In C ++ 11 wird die Hilfsklasse std :: enable_shared_from_this eingeführt. Für einen Entwickler, der erfolgreich Code ohne std :: enable_shared_from_this erstellt, sind die möglichen Verwendungen dieser Klasse möglicherweise nicht offensichtlich.
Was macht std :: enable_shared_from_this?
Es ermöglicht Mitgliedsfunktionen der Klasse, die in std :: shared_ptr instanziiert ist, zusätzliche starke (shared_from_this ()) oder schwache (schwach_from_this (), beginnend mit C ++ 17) Kopien des std :: shared_ptr, in dem es erstellt wurde . Sie können shared_from_this () und schwach_from_this () nicht vom Konstruktor und Destruktor aus aufrufen.

Warum so schwer? Sie können einfach std :: shared_ptr <T> (this) erstellen.
Nein, geht nicht. Alle std :: shared_ptrs, die sich um dieselbe Instanz der Klasse kümmern, müssen eine Verbindungszähleinheit verwenden. Ohne besondere Magie geht es nicht.

Voraussetzung für die Verwendung von std :: enable_shared_from_this ist das erstmalige Erstellen eines Klassenobjekts in std :: shared_ptr. Erstellen auf dem Stapel, dynamisches Zuweisen auf dem Heap, Erstellen auf std :: unique_ptr - all dies ist nicht geeignet. Nur streng in std :: shared_ptr.

Ist es möglich, den Benutzer beim Erstellen von Instanzen der Klasse einzuschränken?
Ja, das kannst du. Um dies zu tun, einfach:
- Bereitstellung einer statischen Methode zum Erstellen von Instanzen, die ursprünglich in std :: shared_ptr platziert wurden;
- den Konstruktor privat oder geschützt stellen;
- Kopier- und Verschiebungssemantik verbieten.
Die Klasse ging in den Käfig, schloss ihn ab und schluckte den Schlüssel - von nun an leben alle seine Instanzen nur noch in std :: shared_ptr, und es gibt keine legalen Möglichkeiten, sie dort rauszuholen.
Eine solche Einschränkung kann nicht als gute architektonische Lösung bezeichnet werden, aber diese Methode entspricht vollständig dem Standard.
Darüber hinaus können Sie das PIMPL-Idiom verwenden: Der einzige Benutzer der launischen Klasse - die Fassade - erstellt die Implementierung ausschließlich in std :: shared_ptr, und der Fassade selbst werden bereits solche Einschränkungen entzogen.

std :: enable_shared_from_this weist erhebliche Vererbungsnuancen auf, deren Erörterung jedoch den Rahmen dieses Artikels sprengt.

Komm auf den Punkt


Alle im Artikel bereitgestellten Codebeispiele werden auf dem Github veröffentlicht .
Der Code zeigt schlechte Techniken, die als die übliche sichere Verwendung von modernem C ++ getarnt sind

Simplecyclic


Es scheint, dass nichts auf Probleme hindeutet. Eine Klassendeklaration sieht einfach und unkompliziert aus. Mit Ausnahme eines „kleinen“ Details wird aus irgendeinem Grund die Vererbung von std :: enable_shared_from_this angewendet.

SimpleCyclic.h
#pragma once #include <memory> #include <functional> namespace SimpleCyclic { class Cyclic final : public std::enable_shared_from_this<Cyclic> { public: static std::shared_ptr<Cyclic> create(); Cyclic(const Cyclic&) = delete; Cyclic(Cyclic&&) = delete; Cyclic& operator=(const Cyclic&) = delete; Cyclic& operator=(Cyclic&&) = delete; ~Cyclic(); void doSomething(); private: Cyclic(); std::function<void(void)> _fn; }; } // namespace SimpleCyclic 


Und in der Umsetzung:

SimpleCyclic.cpp
 #include <iostream> #include "SimpleCyclic.h" namespace SimpleCyclic { Cyclic::Cyclic() = default; Cyclic::~Cyclic() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } std::shared_ptr<Cyclic> Cyclic::create() { return std::shared_ptr<Cyclic>(new Cyclic); } void Cyclic::doSomething() { _fn = [shis = shared_from_this()](){}; std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } } // namespace SimpleCyclic 


main.cpp
 #include "SimpleCyclic/SimpleCyclic.h" int main() { auto simpleCyclic = SimpleCyclic::Cyclic::create(); simpleCyclic->doSomething(); return 0; } 


Konsolenausgabe
N12SimpleCyclic6CyclicE :: doSomething


Im Hauptteil der Funktion doSomething () erstellt die Klasseninstanz selbst eine zusätzliche starke Kopie des std :: shared_ptr, in dem sie platziert wurde. Dann wird diese Kopie unter Verwendung einer verallgemeinerten Erfassung in eine Lambda-Funktion gelegt, die dem Klassendatenfeld unter dem Deckmantel einer harmlosen std :: -Funktion zugewiesen ist. Ein Aufruf von doSomething () führt zu einem Zirkelverweis, und die Klasseninstanz wird auch nach der Zerstörung aller externen starken Links nicht mehr zerstört.
Es liegt ein Speicherverlust vor. Der SimpleCyclic :: Cyclic :: ~ Cyclic-Destruktor wird nicht aufgerufen.

Die Klasseninstanz "behält" sich selbst.
Der Code blieb in einem Knoten stecken.


(Bild von hier aufgenommen )

Und was, das ist das "Zombie" Antimuster?
Nein, das ist nur ein Training. Das Interessanteste steht noch bevor.

Warum hat der Entwickler das geschrieben?
Synthetisches Beispiel. Mir sind keine Situationen bekannt, in denen ein solcher Code harmonisch erhalten würde.

Hat die dynamische Code-Analyse also geschwiegen?
Nein, Valgrind hat ehrlich gesagt einen Speicherverlust gemeldet:

Post Valgrind
96 (64 direkte, 32 indirekte) Bytes in 1 Blöcken gehen definitiv im Verlustrekord 29 von 46 verloren
in SimpleCyclic :: Cyclic :: create () in /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp:15
1: malloc in /usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so
2: Operator neu (vorzeichenlos lang) in /usr/lib/libc++abi.dylib
3: SimpleCyclic :: Cyclic :: create () in /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp:15
4: main in /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/main.cpphaps


Pimplcyclic


In diesem Fall sieht die Header-Datei vollständig korrekt und präzise aus. Es wurde eine Fassade deklariert, die eine bestimmte Implementierung in std :: shared_ptr speichert. Die Vererbung - auch von std :: enable_shared_from_this - fehlt im Gegensatz zum vorherigen Beispiel.

Pimplcyclic.h
 #pragma once #include <memory> namespace PimplCyclic { class Cyclic { public: Cyclic(); ~Cyclic(); private: class Impl; std::shared_ptr<Impl> _impl; }; } // namespace PimplCyclic 


Und in der Umsetzung:

Pimplcyclic.cpp
 #include <iostream> #include <functional> #include "PimplCyclic.h" namespace PimplCyclic { class Cyclic::Impl : public std::enable_shared_from_this<Cyclic::Impl> { public: ~Impl() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } void doSomething() { _fn = [shis = shared_from_this()](){}; std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } private: std::function<void(void)> _fn; }; Cyclic::Cyclic() : _impl(std::make_shared<Impl>()) { if (_impl) { _impl->doSomething(); } } Cyclic::~Cyclic() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } } // namespace PimplCyclic 


main.cpp
 #include "PimplCyclic/PimplCyclic.h" int main() { auto pimplCyclic = PimplCyclic::Cyclic(); return 0; } 


Konsolenausgabe
N11PimplCyclic6Cyclic4ImplE :: doSomething
N11PimplCyclic6CyclicE :: ~ Cyclic


Durch Aufrufen von Impl :: doSomething () wird ein Zirkelverweis in einer Instanz der Impl-Klasse erstellt. Die Fassade ist korrekt zerstört, aber die Implementierung ist undicht. Der Destruktor PimplCyclic :: Cyclic :: Impl :: ~ Impl wird nicht aufgerufen.
Das Beispiel ist wieder synthetisch, aber diesmal gefährlicher - alle schlechten Geräte befinden sich in der Implementierung und erscheinen nicht in der Anzeige.
Darüber hinaus erforderte der Benutzercode zum Erstellen einer zirkulären Verknüpfung keine andere Aktion als die Konstruktion.
Eine dynamische Analyse angesichts von Valgrind und diesmal ergab ein Leck:

Post Valgrind
96 Bytes in 1 Blöcken gehen definitiv im Verlustrekord 29 von 46 verloren
in PimplCyclic :: Cyclic :: Cyclic () in /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28
1: malloc in /usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so
2: Operator neu (vorzeichenlos lang) in /usr/lib/libc++abi.dylib
3: std :: __ 1 :: __ libcpp_allocate (Long ohne Vorzeichen, Long ohne Vorzeichen) in /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/new:252
4: std :: __ 1 :: allocator <std :: __ 1 :: __ shared_ptr_emplace <PimplCyclic :: Cyclic :: Impl, std :: __ 1 :: allocator <PimplCyclic :: Cyclic :: Impl >>> allocate (unsigned long , void const *) in /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:1813
5: std :: __ 1 :: shared_ptr <PimplCyclic :: Cyclic :: Impl> std :: __ 1 :: shared_ptr <PimplCyclic :: Cyclic :: Impl> :: make_shared <> () in /Applications/Xcode.app/Contents /Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:4326
6: _ZNSt3__1L11make_sharedIN11PimplCyclic6Cyclic4ImplEJEEENS_9enable_ifIXntsr8is_arrayIT_EE5valueENS_10shared_ptrIS5_EEE4typeEDpOT0_ in /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:4706
7: PimplCyclic :: Cyclic :: Cyclic () in /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28
8: PimplCyclic :: Cyclic :: Cyclic () in /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:29
9: main in /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/main.cpphaps


Es ist etwas verdächtig, Pimpl zu sehen, in dem die Implementierung in std :: shared_ptr gespeichert ist.
Der klassische Pimpl, der auf einem Rohzeiger basiert, ist zu archaisch, und std :: unique_ptr hat den Nebeneffekt, dass das Verbot der Kopiersemantik auf der Fassade verbreitet wird. Eine solche Fassade wird die Redewendung des alleinigen Eigentums umsetzen, die möglicherweise nicht der architektonischen Idee entspricht. Aus der Verwendung von std :: shared_ptr zum Speichern der Implementierung schließen wir, dass die Klasse so konzipiert ist, dass sie eine gemeinsame Eigentümerschaft bietet.

Wie unterscheidet sich dies von der klassischen Leckage - Speicherzuweisung durch expliziten Aufruf von new ohne nachträgliches Löschen? Ebenso wäre alles schön in der Oberfläche und in der Implementierung - ein Fehler.
Wir diskutieren moderne Möglichkeiten, sich in den Fuß zu schießen.

Antipattern "Zombies"


Aus dem obigen Material geht also klar hervor:
- Intelligente Zeiger können in Knoten eingebunden werden.
- Die Verwendung von std :: enable_shared_from_this kann dazu beitragen, weil Ermöglicht es einer Instanz einer Klasse, sich ohne fremde Hilfe an einen Knoten zu binden.

Und jetzt - Aufmerksamkeit - die Schlüsselfrage des Artikels: Ist die Art der Ressource, die in einen intelligenten Zeiger eingeschlossen ist, von Bedeutung? Gibt es einen Unterschied zwischen einer RAII-Dateipflege und einer asynchronen HTTPS-Verbindung?

Simplezomby


Der Code, der allen nachfolgenden Beispielen von Zombies gemeinsam ist, wurde in die Common-Bibliothek verschoben.

Abstrakte Zombie-Schnittstelle mit dem bescheidenen Namen Manager:

Common / Manager.h
 #pragma once #include <memory> namespace Common { class Listener; class Manager { public: Manager() = default; Manager(const Manager&) = delete; Manager(Manager&&) = delete; Manager& operator=(const Manager&) = delete; Manager& operator=(Manager&&) = delete; virtual ~Manager() = default; virtual void runOnce(std::shared_ptr<Common::Listener> listener) = 0; }; } // namespace Common 


Abstrakte Oberfläche des Hörers, bereit, threadsicheren Text zu akzeptieren:

Common / Listener.h
 #pragma once #include <string> #include <memory> namespace Common { class Listener { public: virtual ~Listener() = default; using Data = std::string; // thread-safe virtual void processData(const std::shared_ptr<const Data> data) = 0; }; } // namespace Common 


Listener, der der Konsole Text anzeigt. Implementiert das SingletonShared-Konzept aus meinem Artikel Technik zum Vermeiden von undefiniertem Verhalten beim Aufrufen eines Singleton :

Common / Impl / WriteToConsoleListener.h
 #pragma once #include <mutex> #include "Common/Listener.h" namespace Common { class WriteToConsoleListener final : public Listener { public: WriteToConsoleListener(const WriteToConsoleListener&) = delete; WriteToConsoleListener(WriteToConsoleListener&&) = delete; WriteToConsoleListener& operator=(const WriteToConsoleListener&) = delete; WriteToConsoleListener& operator=(WriteToConsoleListener&&) = delete; ~WriteToConsoleListener() override; static std::shared_ptr<WriteToConsoleListener> instance(); // blocking void processData(const std::shared_ptr<const Data> data) override; private: WriteToConsoleListener(); std::mutex _mutex; }; } // namespace Common 


Common / Impl / WriteToConsoleListener.cpp
 #include <iostream> #include "WriteToConsoleListener.h" namespace Common { WriteToConsoleListener::WriteToConsoleListener() = default; WriteToConsoleListener::~WriteToConsoleListener() { auto lock = std::lock_guard(_mutex); std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } std::shared_ptr<WriteToConsoleListener> WriteToConsoleListener::instance() { static auto inst = std::shared_ptr<WriteToConsoleListener>(new WriteToConsoleListener); return inst; } void WriteToConsoleListener::processData(const std::shared_ptr<const Data> data) { if (data) { auto lock = std::lock_guard(_mutex); std::cout << *data << std::flush; } } } // namespace Common 


Und schließlich der erste Zombie, der einfachste und genialste.

SimpleZomby.h
 #pragma once #include <memory> #include <atomic> #include <thread> #include "Common/Manager.h" namespace Common { class Listener; } // namespace Common namespace SimpleZomby { class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby> { public: static std::shared_ptr<Zomby> create(); ~Zomby() override; void runOnce(std::shared_ptr<Common::Listener> listener) override; private: Zomby(); using Semaphore = std::atomic<bool>; std::shared_ptr<Common::Listener> _listener; Semaphore _semaphore = false; std::thread _thread; }; } // namespace SimpleZomby 


SimpleZomby.cpp
 #include <sstream> #include "SimpleZomby.h" #include "Common/Listener.h" namespace SimpleZomby { std::shared_ptr<Zomby> Zomby::create() { return std::shared_ptr<Zomby>(new Zomby()); } Zomby::Zomby() = default; Zomby::~Zomby() { _semaphore = false; if (_thread.joinable()) { _thread.detach(); } if (_listener) { std::ostringstream buf; buf << typeid(*this).name() << "::" << __func__ << std::endl; _listener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) { if (_semaphore) { throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice"); } _listener = listener; _semaphore = true; _thread = std::thread([shis = shared_from_this()](){ while (shis && shis->_listener && shis->_semaphore) { shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n")); std::this_thread::sleep_for(std::chrono::seconds(1)); } }); } } // namespace SimpleZomby 


Ein Zombie führt eine Lambda-Funktion in einem separaten Thread aus und sendet regelmäßig eine Zeichenfolge an den Listener. Lambda-Funktionen für die Arbeit benötigen ein Semaphor und einen Listener, die Felder der Zombie-Klasse sind. Die Lambda-Funktion erfasst sie nicht als separate Felder, sondern verwendet das Objekt als Aggregator. Das Zerstören einer Instanz der Zombie-Klasse vor Abschluss der Lambda-Funktion führt zu undefiniertem Verhalten. Um dies zu vermeiden, erfasst die Lambda-Funktion eine starke Kopie von shared_from_this ().
Im Zombie-Destruktor wird das Semaphor auf false gesetzt, woraufhin attach () für den Stream aufgerufen wird. Durch das Festlegen des Semaphors wird der Thread angewiesen, herunterzufahren.

Im Destruktor musste nicht separ (), sondern join () aufgerufen werden!
... und erhalten Sie einen Destruktor, der die Ausführung auf unbestimmte Zeit blockiert, was möglicherweise nicht akzeptabel ist.

Das ist also eine Verletzung von RAII! RAII sollte den Destruktor erst nach Freigabe der Ressource verlassen!
Wenn streng - dann ja, der Zombie-Destruktor gibt die Ressource nicht frei, sondern garantiert nur , dass die Freigabe erfolgt . Irgendwann produziert - vielleicht bald oder vielleicht nicht wirklich. Und es ist sogar möglich, dass main die Arbeit früher beendet - dann wird der Thread vom Betriebssystem zwangsweise gelöscht. Tatsächlich kann die Grenze zwischen „richtig“ und „falsch“ RAII jedoch sehr dünn sein: Beispielsweise kann „korrektes“ RAII, das std :: filesystem :: remove () in einem Destruktor für eine temporäre Datei aufruft, die Kontrolle darüber zurückgeben Der Moment, in dem sich der Schreibbefehl noch in einem der flüchtigen Caches befindet und nicht ehrlich auf die Magnetplatte der Festplatte geschrieben wird.

main.cpp
 #include <chrono> #include <thread> #include <sstream> #include "Common/Impl/WriteToConsoleListener.h" #include "SimpleZomby/SimpleZomby.h" int main() { auto writeToConsoleListener = Common::WriteToConsoleListener::instance(); { auto simpleZomby = SimpleZomby::Zomby::create(); simpleZomby->runOnce(writeToConsoleListener); std::this_thread::sleep_for(std::chrono::milliseconds(4500)); } // Zomby should be killed here { std::ostringstream buf; buf << "============================================================\n" << "| Zomby was killed |\n" << "============================================================\n"; if (writeToConsoleListener) { writeToConsoleListener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } std::this_thread::sleep_for(std::chrono::milliseconds(5000)); return 0; } 


Konsolenausgabe
SimpleZomby lebt!
SimpleZomby lebt!
SimpleZomby lebt!
SimpleZomby lebt!
SimpleZomby lebt!
================================================== ===========
| Zomby wurde getötet
================================================== ===========
SimpleZomby lebt!
SimpleZomby lebt!
SimpleZomby lebt!
SimpleZomby lebt!
SimpleZomby lebt!


Was kann aus der Ausgabe des Programms gesehen werden:
- Der Zombie arbeitete auch nach Verlassen des Sichtfeldes weiter.
- Weder für Zombies noch für WriteToConsoleListener wurden Destruktoren aufgerufen.
Ein Speicherverlust ist aufgetreten.
Es gab ein Ressourcenleck. Und die Ressource ist in diesem Fall der Thread der Ausführung.
Der Code, der aufhören sollte, funktionierte weiterhin in einem separaten Thread.
Ein WriteToConsoleListener-Leck hätte durch die Verwendung der SingletonWeak-Technik aus meinem Artikel Vermeiden von unbestimmtem Verhalten beim Aufrufen eines Singleton verhindert werden können , aber ich habe dies absichtlich nicht getan.


(Bild von hier aufgenommen )

Warum Zombies?
Weil er getötet wurde und noch lebt.

Wie unterscheidet sich dies von den Zirkelverweisen in den vorherigen Beispielen?
Die Tatsache, dass eine verlorene Ressource nicht nur ein Teil des Speichers ist, sondern etwas, das Code unabhängig von dem Thread ausführt, der ihn gestartet hat.

Ist es möglich, die "Zombies" zu zerstören?
Nach dem Verlassen des Bereichs (d. H. Nach dem Zerstören aller externen starken und schwachen Verweise auf Zombies) ist dies unmöglich. Ein Zombie wird zerstört, wenn er beschließt, sich selbst zu zerstören (ja, es ist etwas mit aktivem Verhalten), vielleicht nie, d. H. bleibt so lange bestehen, bis das Betriebssystem bereinigt wird, wenn die Anwendung beendet wird. Natürlich kann Benutzercode einen gewissen Einfluss auf die Bedingung zum Beenden des Zombie-Codes haben, aber dieser Effekt ist indirekt und implementierungsabhängig.

Und bevor Sie den Bereich verlassen?
Sie können den Zombie-Destruktor explizit aufrufen, aber es ist unwahrscheinlich, dass Sie undefiniertes Verhalten aufgrund der wiederholten Zerstörung des Objekts durch den Smart-Pointer-Destruktor vermeiden können - dies ist ein Kampf gegen RAII. Oder Sie können die Funktion der expliziten De-Initialisierung hinzufügen - und dies ist eine Ablehnung von RAII.

Wie unterscheidet sich das vom Starten eines Threads, gefolgt von remove ()?
Im Fall von Zombies besteht im Gegensatz zu einem einfachen Aufruf zum Lösen () die Idee, den Fluss zu stoppen. Nur funktioniert es nicht. Die richtige Idee hilft, das Problem zu maskieren.

Ist das Beispiel noch synthetisch?
Zum Teil. In diesem einfachen Beispiel gab es nicht genügend Gründe, shared_from_this () zu verwenden. Sie können beispielsweise schwach_from_this () erfassen oder alle erforderlichen Felder in der Klasse erfassen. Mit der Komplexität der Aufgabe kann sich das Gleichgewicht jedoch zur Seite verschieben
shared_from_this ().

Valgrind, Valgrind! Wir haben eine zusätzliche Verteidigungslinie gegen Zombies!
Ach und ah - aber Valgrind hat kein Speicherleck aufgedeckt. Warum - ich weiß es nicht. In der Diagnose gibt es nur „möglicherweise verlorene“ Einträge, die auf Systemfunktionen hinweisen - ungefähr die gleiche und ungefähr die gleiche Menge wie beim Ausarbeiten einer leeren Hauptleitung. Es gibt keine Benutzercode-Referenzen. Andere dynamische Analysetools sind möglicherweise besser, aber wenn Sie sich immer noch auf sie verlassen, lesen Sie weiter.

Steppingzomby


Der Code in diesem Beispiel führt die Schritte auf: resolveDnsName ---> connectTcp ---> EstablishSsl ---> sendHttpRequest ---> readHttpReply und simuliert den Betrieb der Client-HTTPS-Verbindung in asynchroner Ausführung. Jeder Schritt dauert ungefähr eine Sekunde.

Steppingzomby.h
 #pragma once #include <memory> #include <atomic> #include <thread> #include "Common/Manager.h" namespace Common { class Listener; } // namespace Common namespace SteppingZomby { class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby> { public: static std::shared_ptr<Zomby> create(); ~Zomby() override; void runOnce(std::shared_ptr<Common::Listener> listener) override; private: Zomby(); using Semaphore = std::atomic<bool>; std::shared_ptr<Common::Listener> _listener; Semaphore _semaphore = false; std::thread _thread; void resolveDnsName(); void connectTcp(); void establishSsl(); void sendHttpRequest(); void readHttpReply(); }; } // namespace SteppingZomby 


Steppingzomby.cpp
 #include <sstream> #include <string> #include "SteppingZomby.h" #include "Common/Listener.h" namespace { void doSomething(Common::Listener& listener, std::string&& callingFunctionName) { listener.processData(std::make_shared<Common::Listener::Data>(callingFunctionName + " started\n")); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); listener.processData(std::make_shared<Common::Listener::Data>(callingFunctionName + " finished\n")); } } // namespace namespace SteppingZomby { Zomby::Zomby() = default; std::shared_ptr<Zomby> Zomby::create() { return std::shared_ptr<Zomby>(new Zomby()); } Zomby::~Zomby() { _semaphore = false; if (_thread.joinable()) { _thread.detach(); } if (_listener) { std::ostringstream buf; buf << typeid(*this).name() << "::" << __func__ << std::endl; _listener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) { if (_semaphore) { throw std::runtime_error("SteppingZomby::Zomby::runOnce() called twice"); } _listener = listener; _semaphore = true; _thread = std::thread([shis = shared_from_this()](){ if (shis && shis->_listener && shis->_semaphore) { shis->resolveDnsName(); } if (shis && shis->_listener && shis->_semaphore) { shis->connectTcp(); } if (shis && shis->_listener && shis->_semaphore) { shis->establishSsl(); } if (shis && shis->_listener && shis->_semaphore) { shis->sendHttpRequest(); } if (shis && shis->_listener && shis->_semaphore) { shis->readHttpReply(); } }); } void Zomby::resolveDnsName() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } void Zomby::connectTcp() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } void Zomby::establishSsl() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } void Zomby::sendHttpRequest() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } void Zomby::readHttpReply() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } } // namespace SteppingZomby 


main.cpp
 #include <chrono> #include <thread> #include <sstream> #include "SteppingZomby/SteppingZomby.h" #include "Common/Impl/WriteToConsoleListener.h" int main() { auto writeToConsoleListener = Common::WriteToConsoleListener::instance(); { auto steppingZomby = SteppingZomby::Zomby::create(); steppingZomby->runOnce(writeToConsoleListener); std::this_thread::sleep_for(std::chrono::milliseconds(1500)); } // Zombies should be killed here { std::ostringstream buf; buf << "============================================================\n" << "| Zomby was killed |\n" << "============================================================\n"; if (writeToConsoleListener) { writeToConsoleListener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } std::this_thread::sleep_for(std::chrono::milliseconds(5000)); return 0; } 


Konsolenausgabe
N13SteppingZomby5ZombyE :: resolveDnsName gestartet
N13SteppingZomby5ZombyE :: resolveDnsName beendet
N13SteppingZomby5ZombyE :: connectTcp wurde gestartet
================================================== ===========
| Zomby wurde getötet
================================================== ===========
N13SteppingZomby5ZombyE :: connectTcp fertig
N13SteppingZomby5ZombyE :: etablSsl wurde gestartet
N13SteppingZomby5ZombyE :: etablSsl fertig
N13SteppingZomby5ZombyE :: sendHttpRequest gestartet
N13SteppingZomby5ZombyE :: sendHttpRequest beendet
N13SteppingZomby5ZombyE :: readHttpReply gestartet
N13SteppingZomby5ZombyE :: readHttpReply beendet
N13SteppingZomby5ZombyE :: ~ Zomby
N6Common22WriteToConsoleListenerE :: ~ WriteToConsoleListener


Wie im vorherigen Beispiel führte ein Aufruf von runOnce () zu einem Zirkelverweis.
Diesmal wurden jedoch die Destruktoren Zomby und WriteToConsoleListener aufgerufen. Alle Ressourcen wurden korrekt freigegeben, bis die Anwendung beendet wurde. Ein Speicherverlust ist nicht aufgetreten.

Was ist dann das Problem?
Das Problem ist, dass der Zombie zu lange lebte - ungefähr dreieinhalb Sekunden nach der Zerstörung aller externen starken und schwachen Verbindungen zu ihm. Etwa drei Sekunden länger als er hätte leben sollen. Und die ganze Zeit war er damit beschäftigt, die Implementierung der HTTPS-Verbindung zu fördern - bis er sie zu Ende brachte. Trotz der Tatsache, dass das Ergebnis nicht mehr benötigt wurde. Trotz der Tatsache, dass die überlegene Geschäftslogik versuchte, die Zombies aufzuhalten.

Denken Sie darüber nach, Sie haben die Antwort, die Sie nicht brauchen ...
Im Falle einer Client-HTTPS-Verbindung können die Konsequenzen für uns folgende sein:
- Speicherverbrauch;
- CPU-Verbrauch;
- TCP-Portverbrauch;
- die Bandbreite des Kommunikationskanals (sowohl die Anforderung als auch die Antwort können ein Volumen in Megabyte sein);
- Unerwartete Daten können den Betrieb der übergeordneten Geschäftslogik stören - bis zum Übergang zum falschen Ausführungszweig oder zu undefiniertem Verhalten, weil Antwortverarbeitungsmechanismen können bereits zerstört sein.
Und auf der Remote-Seite (nicht vergessen - die HTTPS-Anfrage war für jemanden bestimmt) - genau die gleiche Verschwendung von Ressourcen, und es ist möglich:
- Veröffentlichung von Fotos von Katzen auf einer Unternehmenswebsite;
- Deaktivieren der Fußbodenheizung in Ihrer Küche;
- Ausführung eines Handelsauftrags an der Börse;
- Geldüberweisung von Ihrem Konto;
- Start einer Interkontinentalrakete.
Die Geschäftslogik versuchte, die Zombies zu stoppen, indem sie alle starken und schwachen Verbindungen zu ihnen entfernte. Der Stopp des Fortschritts der HTTPS-Anforderung sollte erfolgen - es war nicht zu spät, die Daten auf Anwendungsebene wurden noch nicht gesendet.
Aber die Zombies entschieden auf ihre eigene Weise.

Geschäftslogik kann anstelle von Zombies neue Objekte erstellen und erneut versuchen, diese zu zerstören, wodurch der Ressourcenverbrauch vervielfacht wird.
Im Fall eines kontinuierlichen Prozesses (z. B. einer Websocket-Verbindung) kann die Verschwendung von Ressourcen stundenlang andauern, und wenn in der Implementierung ein Mechanismus zum automatischen Wiederverbinden vorhanden ist, wenn die Verbindung getrennt wird, kann dieser im Allgemeinen gestoppt werden.

Valgrind?
Keine Chance. Alles wird korrekt freigegeben und aufgeräumt. Spät und nicht vom Hauptfaden, aber völlig korrekt.

Boozdedzomby


In diesem Beispiel wird die boozd :: azzio-Bibliothek verwendet, die eine Nachahmung von boost :: asio ist. Trotz der Tatsache, dass die Nachahmung eher grob ist, können wir das Wesentliche des Problems demonstrieren. Die Bibliothek hat eine Funktion io_context :: async_read (im Original ist sie kostenlos, ändert aber nichts an der Essenz), die Folgendes akzeptiert:
- Stream, aus dem Daten stammen können;
- einen Puffer, mit dem Sie diese Daten sammeln können;
— callback-, .
io_context::async_read callback, (, ). io_context::run() ( , ).

buffer.h
 #pragma once #include <vector> namespace boozd::azzio { using buffer = std::vector<int>; } // namespace boozd::azzio 


stream.h
 #pragma once #include <optional> namespace boozd::azzio { class stream { public: virtual ~stream() = default; virtual std::optional<int> read() = 0; }; } // namespace boozd::azzio 


io_context.h
 #pragma once #include <functional> #include <optional> #include "buffer.h" namespace boozd::azzio { class stream; class io_context { public: ~io_context(); enum class error_code {no_error, good_error, bad_error, unknown_error, known_error, well_known_error}; using handler = std::function<void(error_code)>; // Start an asynchronous operation to read a certain amount of data from a stream. // This function is used to asynchronously read a certain number of bytes of data from a stream. // The function call always returns immediately. void async_read(stream& s, buffer& b, handler&& handler); // Run the io_context object's event processing loop. void run(); private: using pack = std::tuple<stream&, buffer&>; using pack_optional = std::optional<pack>; using handler_optional = std::optional<handler>; pack_optional _pack_optional; handler_optional _handler_optional; }; } // namespace boozd::azzio 


io_context.cpp
 #include <iostream> #include <thread> #include <chrono> #include "io_context.h" #include "stream.h" namespace boozd::azzio { io_context::~io_context() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } void io_context::async_read(stream& s, buffer& b, io_context::handler&& handler) { _pack_optional.emplace(s, b); _handler_optional.emplace(std::move(handler)); } void io_context::run() { if (_pack_optional && _handler_optional) { auto& [s, b] = *_pack_optional; using namespace std::chrono; auto start = steady_clock::now(); while (duration_cast<milliseconds>(steady_clock::now() - start).count() < 1000) { if (auto read = s.read()) b.emplace_back(*read); std::this_thread::sleep_for(milliseconds(100)); } (*_handler_optional)(error_code::no_error); } } } // namespace boozd::azzio 


boozd::azzio::stream, :

impl/random_stream.h
 #pragma once #include "boozd/azzio/stream.h" namespace boozd::azzio { class random_stream final : public stream { public: ~random_stream() override; std::optional<int> read() override; }; } // namespace boozd::azzio 


impl/random_stream.cpp
 #include <iostream> #include "random_stream.h" namespace boozd::azzio { boozd::azzio::random_stream::~random_stream() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } std::optional<int> random_stream::read() { if (!(rand() & 0x1)) return rand(); return std::nullopt; } } // namespace boozd::azzio 


BoozdedZomby -. - async_read(), boozd::azzio run(). boozd::azzio ( ) callback-. , , - shared_from_this.

BoozdedZomby.h
 #pragma once #include <memory> #include <atomic> #include <thread> #include "Common/Manager.h" #include "boozd/azzio/buffer.h" #include "boozd/azzio/io_context.h" #include "boozd/azzio/impl/random_stream.h" namespace Common { class Listener; } // namespace Common namespace BoozdedZomby { class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby> { public: static std::shared_ptr<Zomby> create(); ~Zomby() override; void runOnce(std::shared_ptr<Common::Listener> listener) override; private: Zomby(); using Semaphore = std::atomic<bool>; Semaphore _semaphore = false; std::shared_ptr<Common::Listener> _listener; boozd::azzio::random_stream _stream; boozd::azzio::buffer _buffer; boozd::azzio::io_context _context; std::thread _thread; }; } // namespace BoozdedZomby 


BoozdedZomby.cpp
 #include <iostream> #include <sstream> #include "boozd/azzio/impl/random_stream.h" #include "BoozdedZomby.h" #include "Common/Listener.h" namespace BoozdedZomby { Zomby::Zomby() = default; std::shared_ptr<Zomby> Zomby::create() { return std::shared_ptr<Zomby>(new Zomby()); } Zomby::~Zomby() { _semaphore = false; if (_thread.joinable()) { _thread.detach(); } if (_listener) { std::ostringstream buf; buf << typeid(*this).name() << "::" << __func__ << std::endl; _listener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) { if (_semaphore) { throw std::runtime_error("BoozdedZomby::Zomby::runOnce() called twice"); } _listener = listener; _semaphore = true; _thread = std::thread([shis = shared_from_this()]() { while (shis && shis->_semaphore && shis->_listener) { auto handler = [shis](auto errorCode) { if (shis && shis->_listener && errorCode == boozd::azzio::io_context::error_code::no_error) { std::ostringstream buf; buf << "BoozdedZomby has got a fresh data: "; for (auto const &elem : shis->_buffer) buf << elem << ' '; buf << std::endl; shis->_listener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } }; shis->_buffer.clear(); shis->_context.async_read(shis->_stream, shis->_buffer, handler); shis->_context.run(); } }); } } // namespace BoozdedZomby 


main.cpp
 #include <chrono> #include <thread> #include <sstream> #include "BoozdedZomby/BoozdedZomby.h" #include "Common/Impl/WriteToConsoleListener.h" int main() { auto writeToConsoleListener = Common::WriteToConsoleListener::instance(); { auto boozdedZomby = BoozdedZomby::Zomby::create(); boozdedZomby->runOnce(writeToConsoleListener); std::this_thread::sleep_for(std::chrono::milliseconds(4500)); } // Zombies should be killed here { std::ostringstream buf; buf << "============================================================\n" << "| Zomby was killed |\n" << "============================================================\n"; if (writeToConsoleListener) { writeToConsoleListener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } std::this_thread::sleep_for(std::chrono::milliseconds(5000)); return 0; } 


BoozdedZomby has got a fresh data: 1144108930 101027544 1458777923 1115438165 74243042
BoozdedZomby has got a fresh data: 143542612 1131570933
BoozdedZomby has got a fresh data: 893351816 563613512 704877633
BoozdedZomby has got a fresh data: 1551901393 1399125485 1899894091 937186357 590357944 357571490
================================================== ===========
| Zomby was killed |
================================================== ===========
BoozdedZomby has got a fresh data: 1927702196 130060903 1083454666 2118797801 2035308228 824938981
BoozdedZomby has got a fresh data: 2020739063 1635339425 34075629
BoozdedZomby has got a fresh data: 2146319451 500782188 1269406752 884936716 892053144
BoozdedZomby has got a fresh data: 330111137 1723153177 1070477904
BoozdedZomby has got a fresh data: 343098142 280090412 589673557 889688008 2014119113 388471006


run_once() . . , :
— boozdedZomby;
— writeToConsoleListener;
— .
.
.

?
. . boost::asio. , — ( ).

Valgrind?
Vergangenheit. Obwohl es scheint, Lecks zu erkennen.

Zombies in freier Wildbahn


! !
.
HTTP-
Websocket-
boost , BoozdedZomby + SteppingZomby. , . , production — , .

, boost::asio::io_context!
… n (, -), .

:

stackoverflow ,
,


Fazit


, «».

, .

std::thread — .

, .

event-driven, (polling-based).

.

, . std::enable_shared_from_this, ( — ). , : - .

, SteppingZomby. — shared_from_this ( , , — 1 6 ).

— , . .

, , . std::enable_shared_from_this — .

PS: — .

Source: https://habr.com/ru/post/de471326/


All Articles