The Tale of the Dangerous std :: enable_shared_from_this, ou l'anti-modĂšle Zombie

L'article décrit le dangereux motif "Zombies", qui se produit naturellement dans certaines situations lors de l'utilisation de std :: enable_shared_from_this. Le matériau est quelque part à la jonction de la technologie et de l'architecture C ++ modernes.

Présentation


C ++ 11 a fourni au développeur de merveilleux outils pour travailler avec la mémoire - des pointeurs intelligents std :: unique_ptr et un tas de std :: shared_ptr + std :: faiblesse_ptr. L'utilisation de pointeurs intelligents pour plus de commodité et de sécurité l'emporte de loin sur l'utilisation de pointeurs bruts. Les pointeurs intelligents sont largement utilisés dans la pratique, comme permettre au développeur de se concentrer sur des problÚmes de niveau supérieur au suivi de l'exactitude de la création / suppression d'entités créées dynamiquement.
Le modĂšle de classe std :: enable_shared_from_this fait Ă©galement partie du standard, et cela semble plutĂŽt Ă©trange lorsque vous le rencontrez pour la premiĂšre fois.
L'article expliquera comment vous pouvez vous retrouver avec son utilisation.

Programme Ă©ducatif


RAII et pointeurs intelligents
Le but direct des pointeurs intelligents est de prendre soin d'un morceau de RAM allouĂ© sur le tas. Les pointeurs intelligents implĂ©mentent l'idiome RAII (l'acquisition de ressources est l'initialisation) et peuvent facilement ĂȘtre adaptĂ©s pour prendre en charge d'autres types de ressources qui nĂ©cessitent une initialisation et une dĂ©sinitialisation non triviale, telles que:
- fichiers;
- dossiers temporaires sur le disque;
- connexions réseau (http, websockets);
- fils d'exécution (fils);
- mutex;
- autre (ce qui suffit pour la fantaisie).
Pour une telle gĂ©nĂ©ralisation, il suffit d'Ă©crire une classe (en fait, parfois vous ne pouvez mĂȘme pas Ă©crire une classe, mais utilisez simplement deleter - mais aujourd'hui, le conte n'est pas Ă  ce sujet), implĂ©mentant:
- initialisation dans le constructeur ou dans une méthode distincte;
- désinitialisation dans le destructeur,
puis «envelopper» dans le pointeur intelligent approprié, selon le modÚle de propriété requis - joint (std :: shared_ptr) ou sole (std :: unique_ptr). Il en résulte un «RAII à deux couches»: un pointeur intelligent vous permet de transférer / partager la propriété de la ressource, et la classe d'utilisateurs initialise / désinitialise une ressource non standard.
std :: shared_ptr utilise un mĂ©canisme de comptage de liens. La norme dĂ©finit le compteur de liens forts (compte le nombre de copies existantes de std :: shared_ptr) et le compteur de liens faibles (compte le nombre de copies existantes de std :: faiblesse_ptr crĂ©Ă©es pour cette instance de std :: shared_ptr). La prĂ©sence d'au moins un lien fort garantit que la destruction n'a pas encore Ă©tĂ© effectuĂ©e. Cette propriĂ©tĂ© std :: shared_ptr est largement utilisĂ©e pour garantir la validitĂ© d'un objet jusqu'Ă  ce que son utilisation soit terminĂ©e dans toutes les parties du programme. La prĂ©sence d'un maillon faible n'empĂȘche pas la destruction de l'objet et ne permet d'obtenir un maillon fort que jusqu'Ă  sa destruction.
RAII garantit que la libération d'une ressource est beaucoup plus fiable qu'un appel explicite à supprimer / supprimer [] / libre / fermer / réinitialiser / déverrouiller, car:
- vous pouvez simplement oublier l'appel explicite;
- un appel explicite peut ĂȘtre effectuĂ© par erreur plusieurs fois;
- un dĂ©fi explicite est difficile lors de la mise en Ɠuvre de la propriĂ©tĂ© partagĂ©e d'une ressource;
- le mécanisme de promotion de pile en c ++ garantit l'appel de destructeurs pour tous les objets qui sortent du domaine en cas d'exception.
La garantie de désinitialisation dans l'idiome est si importante qu'elle mérite une bonne place au nom de l'idiome avec l'initialisation.
Les pointeurs intelligents présentent également des inconvénients:
- la présence de surcharge en termes de performances et de mémoire (pour la plupart des applications ce n'est pas significatif);
- la possibilité de liens cycliques bloquant la libération de la ressource et entraßnant sa fuite.
Chaque développeur a sûrement lu plus d'une fois des liens circulaires et vu des exemples synthétiques de code problématique.
Le danger peut sembler insignifiant pour les raisons suivantes:
- si la mémoire fuit fréquemment et beaucoup - cela est notable dans sa consommation, et si rarement et peu - alors le problÚme a peu de chances de se manifester au niveau de l'utilisateur final;
- utilise l'analyse de code dynamique pour les fuites (Valgrind, Clang LeakSanitizer, etc.);
- "Je n’écris pas comme ça";
- «mon architecture est correcte»;
"Notre code est en cours de révision."

std :: enable_shared_from_this
En C ++ 11, la classe d'assistance std :: enable_shared_from_this est introduite. Pour un dĂ©veloppeur qui construit avec succĂšs du code sans std :: enable_shared_from_this, les utilisations potentielles de cette classe peuvent ne pas ĂȘtre Ă©videntes.
Que fait std :: enable_shared_from_this?
Il permet aux fonctions membres de la classe qui est instanciée dans std :: shared_ptr de recevoir des copies supplémentaires fortes (shared_from_this ()) ou faibles (faibles_from_this (), à partir de C ++ 17) du std :: shared_ptr dans lequel il a été créé . Vous ne pouvez pas appeler shared_from_this () et faible_from_this () à partir du constructeur et du destructeur.

Pourquoi si dur? Vous pouvez simplement construire std :: shared_ptr <T> (this)
Non, tu ne peux pas. Tous les std :: shared_ptrs qui se soucient de la mĂȘme instance de la classe doivent utiliser une unitĂ© de comptage de liens. Il n'y a aucun moyen de se passer de magie spĂ©ciale.

Une condition préalable à l'utilisation de std :: enable_shared_from_this est de créer initialement un objet de classe dans std :: shared_ptr. Créer sur la pile, allouer dynamiquement sur le tas, créer sur std :: unique_ptr - tout cela ne convient pas. Seulement strictement dans std :: shared_ptr.

Est-il possible de limiter l'utilisateur dans la maniÚre de créer des instances de la classe?
Oui tu peux. Pour ce faire, il suffit:
- fournir une méthode statique pour créer des instances initialement placées dans std :: shared_ptr;
- mettre le constructeur en privé ou protégé;
- interdire la sémantique de copie et de déplacement.
La classe est entrée dans la cage, l'a verrouillée et a avalé la clé - désormais toutes ses instances ne vivront que dans std :: shared_ptr, et il n'y a aucun moyen légal de les faire sortir de là.
Une telle restriction ne peut pas ĂȘtre qualifiĂ©e de bonne solution architecturale, mais cette mĂ©thode est entiĂšrement conforme Ă  la norme.
De plus, vous pouvez utiliser l'idiome PIMPL: le seul utilisateur de la classe capricieuse - la façade - crĂ©era l'implĂ©mentation strictement dans std :: shared_ptr, et la façade elle-mĂȘme sera dĂ©jĂ  privĂ©e de restrictions de ce type.

std :: enable_shared_from_this a des nuances importantes dans l'héritage, mais les discuter dépasse le cadre de cet article.

Aller droit au but


Tous les exemples de code fournis dans l'article sont publiés sur le github .
Le code montre de mauvaises techniques déguisées en utilisation sûre habituelle du C ++ moderne

Simplecyclic


Il semble que rien ne prĂ©sage de problĂšmes. Une dĂ©claration de classe semble simple et directe. À l'exception d'un «petit» dĂ©tail - pour une raison quelconque, l'hĂ©ritage de std :: enable_shared_from_this est appliquĂ©.

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 


Et en cours d'exécution:

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; } 


Sortie console
N12SimpleCyclic6CyclicE :: doSomething


Dans le corps de la fonction doSomething (), l'instance de classe elle-mĂȘme crĂ©era une copie forte supplĂ©mentaire du std :: shared_ptr dans lequel elle a Ă©tĂ© placĂ©e. Ensuite, Ă  l'aide d'une capture gĂ©nĂ©ralisĂ©e, cette copie est placĂ©e dans une fonction lambda affectĂ©e au champ de donnĂ©es de classe sous le couvert d'une fonction std :: inoffensive. Un appel Ă  doSomething () entraĂźne une rĂ©fĂ©rence circulaire et l'instance de classe ne sera plus dĂ©truite mĂȘme aprĂšs la destruction de tous les liens forts externes.
Il y a une fuite de mémoire. Le destructeur cyclique SimpleCyclic :: Cyclic :: ~ n'est pas appelé.

L'instance de classe se «conserve».
Le code s'est coincĂ© dans un nƓud.


(image prise d'ici )

Et quoi, c'est l'anti-modĂšle "Zombie"?
Non, c'est juste une séance d'entraßnement. Le plus intéressant reste à venir.

Pourquoi le développeur a-t-il écrit cela?
Exemple synthétique. Je n'ai connaissance d'aucune situation dans laquelle un tel code serait harmonieusement obtenu.

Alors, l'analyse de code dynamique est-elle restée silencieuse?
Non, Valgrind a honnĂȘtement signalĂ© une fuite de mĂ©moire:

Post Valgrind
96 (64 directs, 32 indirects) octets en 1 bloc sont définitivement perdus dans le record de perte 29 de 46
dans SimpleCyclic :: Cyclic :: create () dans /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp:15
1: malloc dans /usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so
2: nouvel opérateur (long non signé) dans /usr/lib/libc++abi.dylib
3: SimpleCyclic :: Cyclic :: create () dans /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp:15
4: principal dans /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/main.cpphaps


Pimplcyclic


Dans ce cas, le fichier d'en-tĂȘte semble complĂštement correct et concis. Il a dĂ©clarĂ© une façade qui stocke une certaine implĂ©mentation dans std :: shared_ptr. L'hĂ©ritage - y compris de std :: enable_shared_from_this - est manquant, contrairement Ă  l'exemple prĂ©cĂ©dent.

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


Et en cours d'exécution:

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; } 


Sortie console
N11PimplCyclic6Cyclic4ImplE :: doSomething
N11PimplCyclic6CyclicE :: ~ Cyclique


L'appel Ă  Impl :: doSomething () crĂ©e une rĂ©fĂ©rence circulaire dans une instance de la classe Impl. La façade est dĂ©truite correctement, mais la mise en Ɠuvre fuit. Le destructeur PimplCyclic :: Cyclic :: Impl :: ~ Impl n'est pas appelĂ©.
L'exemple est Ă  nouveau synthĂ©tique, mais cette fois plus dangereux - tout le mauvais matĂ©riel se trouve dans la mise en Ɠuvre et n'apparaĂźt pas dans l'annonce.
De plus, pour créer un lien circulaire, le code utilisateur ne nécessitait aucune action autre que la construction.
L'analyse dynamique face à Valgrind, et cette fois a révélé une fuite:

Post Valgrind
96 octets en 1 blocs sont définitivement perdus dans le record de perte 29 de 46
dans PimplCyclic :: Cyclic :: Cyclic () dans /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28
1: malloc dans /usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so
2: nouvel opérateur (long non signé) dans /usr/lib/libc++abi.dylib
3: std :: __ 1 :: __ libcpp_allocate (unsigned long, unsigned long) dans /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 *) dans /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 <> () dans /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_ dans /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:4706
7: PimplCyclic :: Cyclic :: Cyclic () dans /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28
8: PimplCyclic :: Cyclic :: Cyclic () dans /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:29
9: principal dans /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/main.cpphaps


C'est un peu suspect de voir Pimpl, dans lequel l'implémentation est stockée dans std :: shared_ptr.
Le classique Pimpl basĂ© sur un pointeur brut est trop archaĂŻque, et std :: unique_ptr a pour effet secondaire de rĂ©pandre l'interdiction de copie sĂ©mantique sur la façade. Une telle façade mettra en Ɠuvre l'idiome de la propriĂ©tĂ© exclusive, qui peut ne pas correspondre Ă  l'idĂ©e architecturale. De l'utilisation de std :: shared_ptr pour stocker l'implĂ©mentation, nous concluons que la classe est conçue pour fournir une propriĂ©tĂ© partagĂ©e.

En quoi cela diffĂšre-t-il de la fuite classique - allouer de la mĂ©moire en appelant explicitement new sans suppression ultĂ©rieure? De la mĂȘme maniĂšre, tout serait beau dans l'interface et dans l'implĂ©mentation - un bug.
Nous discutons des moyens modernes de vous tirer une balle dans le pied.

Antipattern "Zombies"


Ainsi, d'aprÚs le matériel ci-dessus, il est clair:
- les pointeurs intelligents peuvent ĂȘtre liĂ©s aux nƓuds;
- l'utilisation de std :: enable_shared_from_this peut y contribuer, car permet Ă  une instance d'une classe de se lier Ă  un nƓud sans presque aucune aide extĂ©rieure.

Et maintenant - attention - la question clé de l'article: le type de ressource enveloppé dans un pointeur intelligent est-il important? Existe-t-il une différence entre un traitement de fichier RAII et une connexion HTTPS asynchrone?

Simplezomby


Le code commun à tous les exemples ultérieurs de zombies a été déplacé vers la bibliothÚque commune.

Interface zombie abstraite avec le modeste nom Manager:

Commun / 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 


Interface abstraite de l'auditeur, prĂȘte Ă  accepter du texte thread-safe:

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 


Écouteur qui affiche du texte sur la console. ImplĂ©mente le concept SingletonShared de mon article Technique pour Ă©viter les comportements indĂ©finis lors de l'appel d'un 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 


Et enfin, le premier zombie, le plus simple et le plus ingénu.

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 


Un zombie exécute une fonction lambda dans un thread séparé, envoyant périodiquement une chaßne à l'auditeur. Les fonctions lambda pour le travail ont besoin d'un sémaphore et d'un écouteur, qui sont des champs de la classe zombie. La fonction lambda ne les capture pas en tant que champs séparés, mais utilise l'objet comme agrégateur. La destruction d'une instance de la classe zombie avant la fin de la fonction lambda entraßnera un comportement indéfini. Pour éviter cela, la fonction lambda capture une copie forte de shared_from_this ().
Dans le destructeur de zombies, le sĂ©maphore est dĂ©fini sur false, aprĂšs quoi detach () est appelĂ© pour le flux. La dĂ©finition du sĂ©maphore indique au thread de s'arrĂȘter.

Dans le destructeur, il fallait appeler non pas detach (), mais join ()!
... et obtenez un destructeur qui bloque l'exĂ©cution pour une durĂ©e indĂ©terminĂ©e, ce qui peut ĂȘtre inacceptable.

C'est donc une violation de RAII! RAII ne devait quitter le destructeur qu'aprÚs avoir libéré la ressource!
Si strictement - alors oui, le destructeur de zombies ne libĂšre pas la ressource, mais garantit seulement que la libĂ©ration sera effectuĂ©e . Parfois produit - peut-ĂȘtre bientĂŽt, ou peut-ĂȘtre pas vraiment. Et il est mĂȘme possible que main termine le travail plus tĂŽt - le thread sera alors effacĂ© de force par le systĂšme d'exploitation. Mais en fait, la ligne entre le «bon» et le «mauvais» RAII peut ĂȘtre trĂšs mince: par exemple, le «bon» RAII, qui appelle std :: filesystem :: remove () dans un destructeur pour un fichier temporaire, pourrait bien lui rendre le contrĂŽle. le moment oĂč la commande d'Ă©criture sera toujours dans l'un des caches volatils et ne sera pas honnĂȘtement Ă©crite sur la plaque magnĂ©tique du disque dur.

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; } 


Sortie console
SimpleZomby est vivant!
SimpleZomby est vivant!
SimpleZomby est vivant!
SimpleZomby est vivant!
SimpleZomby est vivant!
=================================================== ===========
| Zomby a été tué |
==================================================== ===========
SimpleZomby est vivant!
SimpleZomby est vivant!
SimpleZomby est vivant!
SimpleZomby est vivant!
SimpleZomby est vivant!


Ce qui peut ĂȘtre vu Ă  la sortie du programme:
- le zombie a continuĂ© Ă  fonctionner mĂȘme aprĂšs avoir quittĂ© le champ de visibilitĂ©;
- aucun destructeur n'a été appelé pour les zombies ou WriteToConsoleListener.
Une fuite de mémoire s'est produite.
Il y a eu une fuite de ressources. Et la ressource dans ce cas est le fil d'exécution.
Le code qui devait s'arrĂȘter a continuĂ© de fonctionner dans un thread sĂ©parĂ©.
Une fuite WriteToConsoleListener aurait pu ĂȘtre Ă©vitĂ©e en utilisant la technique SingletonWeak de mon article Éviter le comportement indĂ©terminĂ© lors de l'appel d'un Singleton , mais je ne l'ai pas fait intentionnellement.


(image prise d'ici )

Pourquoi des zombies?
Parce qu'il a été tué et qu'il est toujours en vie.

En quoi est-ce différent des références circulaires des exemples précédents?
Le fait qu'une ressource perdue n'est pas seulement un morceau de mémoire, mais quelque chose qui exécute indépendamment du code indépendamment du thread qui l'a lancée.

Est-il possible de détruire les "Zombies"?
AprĂšs avoir quittĂ© la portĂ©e (c'est-Ă -dire aprĂšs avoir dĂ©truit toutes les rĂ©fĂ©rences externes fortes et faibles aux zombies) - c'est impossible. Un zombie sera dĂ©truit quand il dĂ©cidera de se dĂ©truire (oui, c'est quelque chose avec un comportement actif), peut-ĂȘtre jamais, c'est-Ă -dire survivra jusqu'Ă  ce que le systĂšme d'exploitation se nettoie Ă  la fin de l'application. Bien sĂ»r, le code utilisateur peut avoir un certain effet sur la condition de sortie du code zombie, mais cet effet sera indirect et dĂ©pend de l'implĂ©mentation.

Et avant de quitter le champ d'application?
Vous pouvez explicitement appeler le destructeur de zombies, mais il est peu probable que vous évitiez un comportement indéfini en raison de la destruction répétée de l'objet par le destructeur de pointeur intelligent également - il s'agit d'un combat contre RAII. Ou vous pouvez ajouter la fonction de désinitialisation explicite - et c'est un rejet de RAII.

En quoi est-ce différent du démarrage d'un thread suivi de detach ()?
Dans le cas des zombies, contrairement Ă  un simple appel Ă  detach (), il y a une idĂ©e d'arrĂȘter le flux. Seulement ça ne marche pas. Avoir la bonne idĂ©e permet de masquer le problĂšme.

L'exemple est-il toujours synthétique?
En partie. Dans cet exemple simple, il n'y avait pas suffisamment de raisons d'utiliser shared_from_this () - par exemple, vous pourriez obtenir en capturant faibles_from_this () ou en capturant tous les champs obligatoires de la classe. Mais avec la complexité de la tùche, l'équilibre peut basculer sur le cÎté
shared_from_this ().

Valgrind, Valgrind! Nous avons une ligne de défense supplémentaire contre les zombies!
HĂ©las et ah - mais Valgrind n'a pas rĂ©vĂ©lĂ© de fuite de mĂ©moire. Pourquoi - je ne sais pas. Dans les diagnostics, il n'y a que des entrĂ©es «éventuellement perdues» qui indiquent les fonctions du systĂšme - Ă  peu prĂšs la mĂȘme et Ă  peu prĂšs la mĂȘme quantitĂ© que lors de l'Ă©laboration d'une alimentation principale vide. Il n'y a aucune rĂ©fĂ©rence de code utilisateur. D'autres outils d'analyse dynamique pourraient faire mieux, mais si vous comptez toujours sur eux, lisez la suite.

Steppingzomby


Le code de cet exemple passe par les étapes resolDnsName ---> connectTcp ---> EstablSsl ---> sendHttpRequest ---> readHttpRhness, simulant le fonctionnement de la connexion HTTPS cliente en exécution asynchrone. Chaque étape prend environ une seconde.

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; } 


Sortie console
N13SteppingZomby5ZombyE :: resolDnsName démarré
N13SteppingZomby5ZombyE :: resolDnsName terminé
N13SteppingZomby5ZombyE :: connectTcp démarré
=================================================== ===========
| Zomby a été tué |
=================================================== ===========
N13SteppingZomby5ZombyE :: connectTcp terminé
N13SteppingZomby5ZombyE :: EstablSsl démarré
N13SteppingZomby5ZombyE :: EstablSsl terminé
N13SteppingZomby5ZombyE :: sendHttpRequest démarré
N13SteppingZomby5ZombyE :: sendHttpRequest terminé
N13SteppingZomby5ZombyE :: readHttpRéponse démarrée
N13SteppingZomby5ZombyE :: readHttpRĂ©fini
N13SteppingZomby5ZombyE :: ~ Zomby
N6Common22WriteToConsoleListenerE :: ~ WriteToConsoleListener


Comme dans l'exemple précédent, un appel à runOnce () a conduit à une référence circulaire.
Mais cette fois, les destructeurs Zomby et WriteToConsoleListener ont été appelés. Toutes les ressources ont été correctement libérées jusqu'à la fin de l'application. Une fuite de mémoire ne s'est pas produite.

Quel est donc le problĂšme?
Le problĂšme est que le zombie a vĂ©cu trop longtemps - environ trois secondes et demie aprĂšs la destruction de tous les liens externes forts et faibles avec lui. Environ trois secondes de plus qu'il n'aurait dĂ» vivre. Et pendant tout ce temps, il s'est engagĂ© Ă  promouvoir la mise en Ɠuvre de la connexion HTTPS - jusqu'Ă  ce qu'il la mette fin. MalgrĂ© le fait que le rĂ©sultat n'Ă©tait plus nĂ©cessaire. MalgrĂ© le fait que la logique commerciale supĂ©rieure a essayĂ© d'arrĂȘter les zombies.

Eh bien, réfléchissez-y, vous avez la réponse dont vous n'avez pas besoin ...
Dans le cas d'une connexion HTTPS client, les consĂ©quences de notre cĂŽtĂ© peuvent ĂȘtre les suivantes:
- consommation de mémoire;
- Consommation CPU;
- Consommation du port TCP;
- la bande passante du canal de communication (la demande et la rĂ©ponse peuvent ĂȘtre un volume en mĂ©gaoctets);
- des donnĂ©es inattendues peuvent perturber le fonctionnement de la logique mĂ©tier de niveau supĂ©rieur - jusqu'Ă  la transition vers la mauvaise branche d'exĂ©cution ou Ă  un comportement indĂ©fini, car les mĂ©canismes de traitement des rĂ©ponses peuvent dĂ©jĂ  ĂȘtre dĂ©truits.
Et du cĂŽtĂ© distant (n'oubliez pas - la requĂȘte HTTPS Ă©tait destinĂ©e Ă  quelqu'un) - exactement le mĂȘme gaspillage de ressources, en plus c'est possible:
- publier des photos de chats sur un site Internet d'entreprise;
- désactiver le chauffage au sol dans votre cuisine;
- exécution d'un ordre commercial sur la bourse;
- transfert d'argent depuis votre compte;
- lancement d'un missile balistique intercontinental.
La logique commerciale a essayĂ© d'arrĂȘter les zombies en supprimant tous les liens forts et faibles avec elle. L'arrĂȘt de l'avancement de la demande HTTPS devait se produire - il n'Ă©tait pas encore trop tard, les donnĂ©es de niveau application n'avaient pas encore Ă©tĂ© envoyĂ©es.
Mais les zombies ont décidé à leur maniÚre.

La logique métier peut créer de nouveaux objets à la place des zombies et essayer à nouveau de les détruire, multipliant ainsi la fuite des ressources.
Dans le cas d'un processus continu (par exemple, une connexion Websocket), le gaspillage de ressources peut se poursuivre pendant des heures et s'il existe un mĂ©canisme de reconnexion automatique dans l'implĂ©mentation lorsque la connexion est dĂ©connectĂ©e, gĂ©nĂ©ralement jusqu'Ă  l'arrĂȘt du programme.

Valgrind?
Aucune chance. Tout est correctement libéré et nettoyé. Tard et pas du fil principal, mais complÚtement correct.

Boozdedzomby


Cet exemple utilise la bibliothÚque boozd :: azzio, qui est une imitation de boost :: asio. Malgré le fait que l'imitation soit assez grossiÚre, elle nous permet de démontrer l'essence du problÚme. io_context::async_read ( , ), :
— stream, ;
— , ;
— 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?
Passé. Bien qu'il semble s'agir de détecter des fuites.

Zombies Ă  l'Ă©tat sauvage


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

, boost::asio::io_context!

 n (, -), .

:

stackoverflow ,
,


Conclusion


, «».

, .

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/fr471326/


All Articles