O conto dos perigosos std :: enable_shared_from_this, ou o antipattern Zombie

O artigo fornece um perigoso “Zombie” antipadrão, que em algumas situações surge naturalmente ao usar std :: enable_shared_from_this. O material está em algum lugar na junção da moderna tecnologia e arquitetura C ++.

1. Introdução


O C ++ 11 forneceu ao desenvolvedor ferramentas maravilhosas para trabalhar com a memória - ponteiros inteligentes std :: unique_ptr e vários std :: shared_ptr + std :: weak_ptr. O uso de ponteiros inteligentes por conveniência e segurança supera em muito o uso de ponteiros brutos. Ponteiros inteligentes são amplamente utilizados na prática, como permita que o desenvolvedor se concentre em questões de nível superior do que controlar a correção da criação / exclusão de entidades criadas dinamicamente.
O modelo de classe std :: enable_shared_from_this também faz parte do padrão e parece bastante estranho quando você o encontra pela primeira vez.
O artigo discutirá como você pode ficar preso ao seu uso.

Programa educacional


RAII e ponteiros inteligentes
O objetivo direto dos ponteiros inteligentes é cuidar de um pedaço de RAM alocado no heap. Ponteiros inteligentes implementam o idioma RAII (aquisição de recursos é inicialização) e podem ser facilmente adaptados para cuidar de outros tipos de recursos que requerem inicialização e desinicialização não trivial, como:
- arquivos;
- pastas temporárias no disco;
- conexões de rede (http, websockets);
- threads de execução (threads);
- mutexes;
- outro (o que é suficiente para fantasia).
Para tal generalização, basta escrever uma classe (de fato, às vezes você nem consegue escrever uma classe, mas apenas usa deleter - mas hoje a história não é sobre isso), implementando:
- inicialização no construtor ou em um método separado;
- desinicialização no destruidor,
em seguida, "envolva" o ponteiro inteligente correspondente, dependendo do modelo de propriedade necessário - joint (std :: shared_ptr) ou sole (std :: unique_ptr). Isso resulta em um "RAII de duas camadas": um ponteiro inteligente permite transferir / compartilhar a propriedade do recurso, e a classe de usuário inicializa / dessinicializa um recurso não padrão.
std :: shared_ptr usa um mecanismo de contagem de links. O padrão define o contador de links fortes (conta o número de cópias existentes de std :: shared_ptr) e o contador de links fracos (conta o número de cópias existentes de std :: weak_ptr criado para esta instância de std :: shared_ptr). A presença de pelo menos um elo forte garante que a destruição ainda não foi feita. Essa propriedade std :: shared_ptr é amplamente usada para garantir a validade de um objeto até que o trabalho seja concluído em todas as partes do programa. A presença de um link fraco não impede a destruição do objeto e permite que você obtenha um link forte apenas até que seja destruído.
O RAII garante que a liberação de um recurso é muito mais confiável do que uma chamada explícita para excluir / excluir [] / liberar / fechar / redefinir / desbloquear, porque:
- você pode simplesmente esquecer a chamada explícita;
- uma chamada explícita pode ser feita erroneamente mais de uma vez;
- um desafio explícito é difícil ao implementar a propriedade compartilhada de um recurso;
- o mecanismo de promoção de pilha no c ++ garante a chamada de destruidores para todos os objetos que ficam fora do escopo em caso de exceção.
A garantia de desinstalação no idioma é tão importante que merece um bom lugar no nome do idioma junto com a inicialização.
Ponteiros inteligentes também têm desvantagens:
- a presença de sobrecarga em termos de desempenho e memória (para a maioria dos aplicativos não é significativa);
- a possibilidade de links cíclicos bloqueando a liberação do recurso e levando ao seu vazamento.
Certamente, todo desenvolvedor mais de uma vez leu sobre links circulares e viu exemplos sintéticos de código problemático.
O perigo pode parecer insignificante pelos seguintes motivos:
- se a memória vazar com frequência e muito - isso é perceptível em seu consumo, e se raramente e pouco -, é improvável que o problema apareça no nível do usuário final;
- usa análise dinâmica de código para vazamentos (Valgrind, Clang LeakSanitizer, etc.);
- "Eu não escrevo assim";
- "minha arquitetura está correta";
"Nosso código está sendo revisado."

std :: enable_shared_from_this
No C ++ 11, a classe auxiliar std :: enable_shared_from_this é introduzida. Para um desenvolvedor que cria código com êxito sem std :: enable_shared_from_this, os usos potenciais dessa classe podem não ser óbvios.
O que std :: enable_shared_from_this faz?
Permite que as funções-membro da classe instanciada em std :: shared_ptr recebam cópias adicionais fortes (shared_from_this ()) ou fracas (fraca_from_this (), a partir do C ++ 17) das cópias std :: shared_ptr nas quais foi criada . Você não pode chamar shared_from_this () e weak_from_this () do construtor e destruidor.

Por que é tão difícil? Você pode simplesmente construir std :: shared_ptr <T> (this)
Não, você não pode. Todos os std :: shared_ptrs que se preocupam com a mesma instância da classe devem usar uma unidade de contagem de links. Não há como ficar sem magia especial.

Um pré-requisito para usar std :: enable_shared_from_this é criar inicialmente um objeto de classe em std :: shared_ptr. Criando na pilha, alocando dinamicamente na pilha, criando em std :: unique_ptr - tudo isso não é adequado. Apenas estritamente em std :: shared_ptr.

É possível limitar o usuário na maneira de criar instâncias da classe?
Sim você pode. Para fazer isso, apenas:
- forneça um método estático para criar instâncias originalmente colocadas em std :: shared_ptr;
- colocar o construtor em privado ou protegido;
- proibir cópia e movimento - semântica.
A classe entrou na gaiola, trancou-a e engoliu a chave - a partir de agora todas as suas instâncias viverão apenas em std :: shared_ptr, e não há maneiras legais de tirá-las de lá.
Essa restrição não pode ser chamada de uma boa solução arquitetural, mas esse método está em total conformidade com o padrão.
Além disso, você pode usar o idioma PIMPL: o único usuário da classe caprichosa - a fachada - criará a implementação estritamente em std :: shared_ptr, e a própria fachada já estará privada de restrições desse tipo.

std :: enable_shared_from_this possui nuances significativas na herança, mas discuti-las está além do escopo deste artigo.

Vá direto ao ponto


Todos os exemplos de código fornecidos no artigo são publicados no github .
O código demonstra más técnicas disfarçadas como o uso seguro usual do C ++ moderno

Simplecyclic


Parece que nada pressagia problemas. Uma declaração de classe parece simples e direta. Exceto por um detalhe “pequeno” - por algum motivo, a herança de std :: enable_shared_from_this é aplicada.

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 


E na implementação:

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


Saída do console
N12SimpleCyclic6CyclicE :: doSomething


No corpo da função doSomething (), a própria instância da classe criará uma cópia forte adicional do std :: shared_ptr no qual foi colocada. Em seguida, usando uma captura generalizada, essa cópia é colocada em uma função lambda atribuída ao campo de dados da classe sob o disfarce de uma função std :: inofensiva. Uma chamada para doSomething () resulta em uma referência circular e a instância da classe não será mais destruída, mesmo após a destruição de todos os links fortes externos.
Há um vazamento de memória. O destruidor SimpleCyclic :: Cyclic :: ~ Cyclic não é chamado.

A instância da classe "mantém" a si mesma.
O código ficou preso em um nó.


(imagem tirada daqui )

E o que, este é o antipadrão "Zombie"?
Não, isso é apenas um treino. Tudo o mais interessante ainda está por vir.

Por que o desenvolvedor escreveu isso?
Exemplo sintético. Não conheço nenhuma situação em que esse código seja obtido harmoniosamente.

Então, a análise dinâmica de código permaneceu silenciosa?
Não, Valgrind relatou honestamente um vazamento de memória:

Post Valgrind
96 (64 diretos, 32 indiretos) bytes em 1 bloco são definitivamente perdidos no registro de perdas 29 de 46
em SimpleCyclic :: Cyclic :: create () em /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp:15
1: malloc em /usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so
2: operador new (long sem assinatura) em /usr/lib/libc++abi.dylib
3: SimpleCyclic :: Cyclic :: create () em /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp:15
4: principal em /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/main.cpphaps


Pimplcyclic


Nesse caso, o arquivo de cabeçalho parece completamente correto e conciso. Ele declarou uma fachada que armazena uma certa implementação em std :: shared_ptr. A herança - incluindo std :: enable_shared_from_this - está ausente, diferente do exemplo anterior.

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


E na implementação:

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


Saída do console
N11PimplCyclic6Cyclic4ImplE :: doSomething
N11PimplCyclic6CyclicE :: ~ Cyclic


Chamar Impl :: doSomething () cria uma referência circular em uma instância da classe Impl. A fachada foi destruída corretamente, mas a implementação está vazando. O destruidor PimplCyclic :: Cyclic :: Impl :: ~ Impl não é chamado.
O exemplo é novamente sintético, mas desta vez mais perigoso - todo o equipamento defeituoso está localizado na implementação e não aparece no anúncio.
Além disso, para criar um link circular, o código do usuário não exigiu nenhuma ação além da construção.
Uma análise dinâmica em face de Valgrind, e desta vez revelou um vazamento:

Post Valgrind
96 bytes em 1 bloco são definitivamente perdidos no registro de perdas 29 de 46
em PimplCyclic :: Cyclic :: Cyclic () em /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28
1: malloc em /usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so
2: operador new (long sem assinatura) em /usr/lib/libc++abi.dylib
3: std :: __ 1 :: __ libcpp_allocate (sem assinatura, sem assinatura) em /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/new:252
4: std :: __ 1 :: alocador <std :: __ 1 :: __ shared_ptr_emplace <PimplCyclic :: Cyclic :: Impl, std :: __ 1 :: alocador <PimplCyclic :: Cyclic :: Impl >>> alocar (sem assinatura por muito tempo , void const *) em /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 <> () em /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_ em /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:4706
7: PimplCyclic :: Cyclic :: Cyclic () em /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28
8: PimplCyclic :: Cyclic :: Cyclic () em /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:29
9: main em /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/main.cpphaps


É um pouco suspeito ver o Pimpl, no qual a implementação é armazenada em std :: shared_ptr.
O Pimpl clássico baseado em um ponteiro bruto é muito arcaico, e std :: unique_ptr tem um efeito colateral de espalhar a proibição de copiar semântica na fachada. Essa fachada implementará o idioma da propriedade exclusiva, que pode não corresponder à idéia arquitetônica. Do uso de std :: shared_ptr para armazenar a implementação, concluímos que a classe foi projetada para fornecer propriedade compartilhada.

Como isso difere do clássico vazamento - alocando memória explicitamente chamando new sem exclusão subseqüente? Da mesma forma, tudo ficaria bonito na interface e na implementação - um bug.
Estamos discutindo maneiras modernas de dar um tiro no próprio pé.

Antipadrão "Zumbis"


Portanto, a partir do material acima, fica claro:
- ponteiros inteligentes podem ser ligados a nós;
- o uso de std :: enable_shared_from_this pode contribuir para isso, porque permite que uma instância de uma classe se vincule a um nó quase sem ajuda externa.

E agora - atenção - a questão-chave do artigo: o tipo de recurso envolvido em um ponteiro inteligente é importante? Existe uma diferença entre um tratamento de arquivo RAII e uma conexão HTTPS assíncrona?

Simplezomby


O código comum a todos os exemplos subseqüentes de zumbis foi movido para a biblioteca Common.

Interface abstrata de zumbi com o modesto nome 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 


Interface abstrata do ouvinte, pronta para aceitar texto seguro para threads:

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 


Ouvinte que exibe texto no console. Implementa o conceito SingletonShared do meu artigo Técnica para evitar comportamentos indefinidos ao chamar um 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 


E, finalmente, o primeiro zumbi, o mais simples e o mais ingênuo.

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 


Um zumbi executa uma função lambda em um thread separado, enviando periodicamente uma string para o ouvinte. As funções do Lambda para trabalho precisam de um semáforo e um ouvinte, que são campos da classe zumbi. A função lambda não os captura como campos separados, mas usa o objeto como um agregador. Destruir uma instância da classe zombie antes que a função lambda seja concluída resultará em um comportamento indefinido. Para evitar isso, a função lambda captura uma cópia forte de shared_from_this ().
No destruidor de zumbis, o semáforo é definido como falso, após o qual detach () é chamado para o fluxo. A configuração do semáforo diz ao thread para desligar.

No destruidor, era necessário chamar não detach (), mas join ()!
... e obtenha um destruidor que bloqueie a execução por tempo indeterminado, o que pode ser inaceitável.

Portanto, isso é uma violação da RAII! O RAII deveria sair do destruidor somente depois de liberar o recurso!
Se estritamente - então sim, o destruidor de zumbis não libera o recurso, mas apenas garante que o lançamento será realizado . Algum tempo produzido - talvez em breve, ou talvez não realmente. E é até possível que o main termine o trabalho mais cedo - então o thread será apagado à força pelo sistema operacional. Mas, na verdade, a linha entre RAII “certo” e “errado” pode ser muito pequena: por exemplo, RAII “correto”, que chama std :: filesystem :: remove () em um destruidor de um arquivo temporário, pode retornar o controle a esse o momento em que o comando write ainda estará em qualquer um dos caches voláteis e não será gravado honestamente na placa magnética do disco rígido.

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


Saída do console
SimpleZomby está vivo!
SimpleZomby está vivo!
SimpleZomby está vivo!
SimpleZomby está vivo!
SimpleZomby está vivo!
===================================================== ===========
| Zomby foi morto |
===================================================== ===========
SimpleZomby está vivo!
SimpleZomby está vivo!
SimpleZomby está vivo!
SimpleZomby está vivo!
SimpleZomby está vivo!


O que pode ser visto na saída do programa:
- o zumbi continuou a trabalhar mesmo depois de sair do campo de visibilidade;
- nenhum destruidor foi chamado para zumbis ou WriteToConsoleListener.
Ocorreu um vazamento de memória.
Houve um vazamento de recursos. E o recurso, neste caso, é o segmento de execução.
O código que deveria parar continuou a funcionar em um encadeamento separado.
Um vazamento WriteToConsoleListener poderia ter sido evitado usando a técnica SingletonWeak do meu artigo Evitando o comportamento indeterminado ao chamar um Singleton , mas intencionalmente não o fiz.


(imagem tirada daqui )

Por que zumbis?
Porque ele foi morto, e ele ainda está vivo.

Como isso é diferente das referências circulares nos exemplos anteriores?
O fato de um recurso perdido não ser apenas um pedaço de memória, mas algo que executa independentemente o código independentemente do encadeamento que o iniciou.

É possível destruir os "zumbis"?
Depois de sair do escopo (ou seja, depois de destruir todas as referências fortes e fracas externas a zumbis) - é impossível. Um zumbi será destruído quando ele decidir se destruir (sim, é algo com comportamento ativo), talvez nunca, ou seja, sobreviverá até que o sistema operacional seja limpo quando o aplicativo terminar. Obviamente, o código do usuário pode ter algum efeito sobre a condição de saída do código zumbi, mas esse efeito será indireto e depende da implementação.

E antes de sair do escopo?
Você pode chamar explicitamente o destruidor de zumbis, mas é improvável que você evite comportamentos indefinidos devido à destruição repetida do objeto pelo destruidor de ponteiro inteligente também - esta é uma luta contra o RAII. Ou você pode adicionar a função de desinicialização explícita - e isso é uma rejeição do RAII.

Como isso é diferente de apenas iniciar um thread seguido de detach ()?
No caso de zumbis, ao contrário de uma simples chamada para desanexar (), existe uma idéia para interromper o fluxo. Só que não funciona. Ter a idéia certa ajuda a mascarar o problema.

O exemplo ainda é sintético?
Em parte. Neste exemplo simples, não havia motivos suficientes para usar shared_from_this () - por exemplo, você poderia capturar o fraco_from_this () ou capturar todos os campos obrigatórios da classe. Mas com a complexidade da tarefa, o equilíbrio pode mudar para o lado
shared_from_this ().

Valgrind, Valgrind! Temos uma linha de defesa adicional contra zumbis!
Ai e ah - mas Valgrind não revelou um vazamento de memória. Por que - eu não sei. Nos diagnósticos, existem apenas entradas "possivelmente perdidas" que indicam as funções do sistema - aproximadamente a mesma e aproximadamente a mesma quantidade que quando se trabalha com uma rede principal vazia. Não há referências de código do usuário. Outras ferramentas de análise dinâmica podem se sair melhor, mas se você ainda confiar nelas, continue lendo.

Steppingzomby


O código neste exemplo segue as etapas resolveDnsName ---> connectTcp ---> estabeleSsl ---> sendHttpRequest ---> readHttpReply, simulando a operação da conexão HTTPS do cliente em execução assíncrona. Cada passo leva cerca de um segundo.

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


Saída do console
N13SteppingZomby5ZombyE :: resolveDnsName iniciado
N13SteppingZomby5ZombyE :: resolveDnsName concluído
N13SteppingZomby5ZombyE :: connectTcp iniciado
===================================================== ===========
| Zomby foi morto |
===================================================== ===========
N13SteppingZomby5ZombyE :: connectTcp finalizado
N13SteppingZomby5ZombyE :: EstablSsl iniciado
N13SteppingZomby5ZombyE :: EstablSsl concluído
N13SteppingZomby5ZombyE :: sendHttpRequest iniciado
N13SteppingZomby5ZombyE :: sendHttpRequest finalizado
N13SteppingZomby5ZombyE :: readHttpReply iniciado
N13SteppingZomby5ZombyE :: readHttpReply concluído
N13SteppingZomby5ZombyE :: ~ Zomby
N6Common22WriteToConsoleListenerE :: ~ WriteToConsoleListener


Como no exemplo anterior, uma chamada para runOnce () levou a uma referência circular.
Mas desta vez, os destruidores de Zomby e WriteToConsoleListener foram chamados. Todos os recursos foram liberados corretamente até o término do aplicativo. Um vazamento de memória não ocorreu.

Qual é o problema então?
O problema é que o zumbi viveu muito tempo - cerca de três segundos e meio após a destruição de todos os elos fortes e fracos externos a ele. Cerca de três segundos a mais do que ele deveria ter vivido. E, durante todo esse tempo, ele se dedicou a promover a implementação da conexão HTTPS - até que ele chegou ao fim. Apesar do fato de o resultado não ser mais necessário. Apesar do fato de que a lógica de negócios superior tentou impedir os zumbis.

Bem, pense bem, você tem a resposta que não precisa ...
No caso de uma conexão HTTPS do cliente, as consequências do nosso lado podem ser as seguintes:
- consumo de memória;
- consumo de CPU;
- consumo de porta TCP;
— ( , );
— - — , .. .
( — HTTPS- - ) — , :
— ;
— ;
— ;
— ;
— .
- , . HTTPS- — , .
-.

- , .
(, Websocket-) , - — .

Valgrind?
. . , .

BoozdedZomby


boozd::azzio, boost::asio. , , . 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?
Passado. Embora pareça ter sido detectar vazamentos.

Zumbis em estado selvagem


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

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

:

stackoverflow ,
,


Conclusão


, «».

, .

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


All Articles