危险std的故事:: enable_shared_from_this或Zombie反模式

本文提供了一个危险的反模式“ Zombie”,在某些情况下,使用std :: enable_shared_from_this时自然会发生这种情况。 该材料位于现代C ++技术和体系结构的交汇处。

引言


C ++ 11为开发人员提供了使用内存的出色工具-智能指针std :: unique_ptr和一堆std :: shared_ptr + std :: weak_ptr。 为了方便和安全使用智能指针远远超过了原始指针的使用。 智能指针在实践中被广泛使用,因为 与跟踪动态创建的实体的创建/删除的正确性相比,允许开发人员将重点放在更高级别的问题上。
std :: enable_shared_from_this类模板也是该标准的一部分,当您初次遇到它时,它似乎很奇怪。
本文将讨论如何使用它。

教育计划


RAII和智能指针
智能指针的直接目的是要照顾在堆上分配的RAM 。 智能指针实现RAII习惯用法(资源获取是初始化),并且可以轻松地进行调整以适应需要初始化和非平凡的反初始化的其他类型的资源,例如:
-文件;
-磁盘上的临时文件夹;
-网络连接(http,websockets);
-执行线程(线程);
-互斥锁;
-其他(足以实现幻想)。
对于这样的概括,编写一个类就足够了(实际上,有时您甚至不能编写一个类,而只是使用deleter-但今天的故事已不涉及该类了),它实现了:
-在构造函数中或在单独的方法中进行初始化;
-在析构函数中取消初始化,
然后根据所需的所有权模型-联合(std :: shared_ptr)或唯一(std :: unique_ptr)将其“包装”到适当的智能指针中。 这导致“两层RAII”:智能指针允许您转移/共享资源所有权,而用户类初始化/取消初始化非标准资源。
std :: shared_ptr使用链接计数机制。 该标准定义了强链接的计数器(计算std :: shared_ptr的现有副本数)和弱链接的计数器(计算为此std :: shared_ptr实例创建的std :: weak_ptr的现有副本数)。 至少一个牢固的链接的存在确保了销毁尚未完成。 此std :: shared_ptr属性广泛用于确保对象的有效性,直到在程序的所有部分中完成对它的使用为止。 弱链接的存在并不能防止对象的破坏,而只能在对象被破坏之前获得强链接。
RAII保证资源释放比显式调用delete / delete [] / free / close / reset / unlock要可靠得多,因为:
-您只需忘记显式调用;
-一个显式的调用可能会多次错误地发出;
-在实现资源的共享所有权时,明确的挑战是困难的;
-c ++中的堆栈提升机制可确保在发生异常的情况下为超出范围的所有对象调用析构函数。
成语中取消初始化的保证是如此重要,以至于它在初始化的同时也应在成语的名称中占有一席之地。
智能指针也有缺点:
-在性能和内存方面存在开销(对于大多数应用程序而言并不重要);
-周期性链接可能阻止资源的释放并导致其泄漏。
当然,每个开发人员都不止一次阅读有关循环链接的内容,并看到了有问题的代码的综合示例。
由于以下原因,该危险似乎微不足道:
-如果内存泄漏频繁且频繁-在消耗上很明显,并且很少或很少-那么问题就不太可能在最终用户级别显现出来;
-对泄漏使用动态代码分析(Valgrind,Clang LeakSanitizer等);
-“我不是那样写的”;
-“我的架构是正确的”;
“我们的代码正在审查中。”

std :: enable_shared_from_this
在C ++ 11中,引入了辅助类std :: enable_shared_from_this。 对于成功构建没有std :: enable_shared_from_this的代码的开发人员,此类的潜在用途可能并不明显。
std :: enable_shared_from_this是做什么的?
它允许在std :: shared_ptr中实例化的类的成员函数接收创建它的std :: shared_ptr的其他强拷贝(shared_from_this())或弱拷贝(weak_from_this(),从C ++ 17开始)。 。 您不能从构造函数和析构函数调用shared_from_this()和weak_from_this()。

为什么这么辛苦? 您可以简单地构造std :: shared_ptr <T>(this)
不,你不能。 所有关心该类的同一实例的std :: shared_ptrs必须使用一个链接计数单元。 没有特殊的魔术是没有办法的。

使用std :: enable_shared_from_this的先决条件是首先在std :: shared_ptr中创建一个类对象。 在堆栈上创建,在堆上动态分配,在std :: unique_ptr上创建-所有这些都不适合。 仅严格在std :: shared_ptr中。

是否可以通过创建类实例的方式来限制用户?
是的,你可以。 为此,只需:
-提供静态方法来创建最初放置在std :: shared_ptr中的实例;
-将构造函数置于私有或受保护的状态;
-禁止复制和移动语义。
该类进入了笼子,将其锁定并吞下了钥匙-从现在开始,其所有实例将仅存在于std :: shared_ptr中,并且没有合法的方法可以将它们移出那里。
这样的限制不能称为好的体系结构解决方案,但是此方法完全符合标准。
另外,您可以使用PIMPL惯用语:反复无常类的唯一用户-Facade-将严格在std :: shared_ptr中创建实现,并且Facade本身已经被取消了这种限制。

std :: enable_shared_from_this在继承方面有很多细微差别,但是讨论它们不在本文讨论范围之内。

切入点


本文提供的所有代码示例均在github上发布。
该代码演示了伪装成现代C ++通常安全使用的不良技术

单循环


似乎没有什么预示着问题。 类声明看起来很简单明了。 除了一个“小”细节外-出于某种原因,将继承自std :: enable_shared_from_this。

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 


并在实施中:

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


控制台输出
N12SimpleCyclic6CyclicE :: doSomething


在doSomething()函数的主体中,类实例本身将创建放置它的std :: shared_ptr的另一个强副本。 然后,使用通用捕获,将此副本放置在以无害std ::函数为幌子分配给类数据字段的lambda函数中。 调用doSomething()会导致循环引用,并且即使在销毁所有外部强链接之后,该类实例也将不再被销毁。
内存泄漏。 不调用SimpleCyclic :: Cyclic ::〜循环析构函数。

类实例“保持”自身。
代码陷入了困境。


(图片来自这里

什么是“僵尸”反模式?
不,这只是一种锻炼。 所有最有趣的事情尚未到来。

开发人员为什么要写这个?
综合示例。 我不知道在任何情况下都能和谐地获得这样的代码。

那么,动态代码分析是否保持沉默?
不,Valgrind诚实地报告了内存泄漏:

后瓦尔格朗德
1块中的96(64个直接,32个间接)字节肯定在46的丢失记录中丢失
在/用户/用户/项目/Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp中的SimpleCyclic :: Cyclic :: create()中
1:在/usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so中的malloc
2:/usr/lib/libc++abi.dylib中的运算符new(无符号长)
3:在/Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp:15中的SimpleCyclic :: Cyclic :: create()
4:主要位于/Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/main.cpphaps


双环


在这种情况下,头文件看起来完全正确且简洁。 它声明了一个在std :: shared_ptr中存储特定实现的外观。 与前面的示例不同,缺少继承-包括从std :: enable_shared_from_this的继承。

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


并在实施中:

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


控制台输出
N11PimplCyclic6Cyclic4ImplE :: doSomething
N11PimplCyclic6CyclicE ::〜循环


调用Impl :: doSomething()在Impl类的实例中创建一个循环引用。 门面已正确销毁,但实现泄漏。 析构函数PimplCyclic :: Cyclic :: Impl ::〜不被调用。
该示例再次是综合示例,但这次更加危险-所有不良设备都位于实现中,并且不会出现在广告中。
此外,要创建循环链接,用户代码除了构造外不需要任何操作。
面对Valgrind进行的动态分析,这次发现了泄漏:

后瓦尔格朗德
1块中的96字节肯定在46的丢失记录中丢失了29
在/用户/用户/项目/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28中的PimplCyclic :: Cyclic :: Cyclic()中
1:在/usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so中的malloc
2:/usr/lib/libc++abi.dylib中的运算符new(无符号长)
3:在/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/new:252中的std :: __ 1 :: __ libcpp_allocate(unsigned long,unsigned long)
4:std :: __ 1 ::分配器<std :: __ 1 :: __ shared_ptr_emplace <PimplCyclic :: Cyclic :: Impl,std :: __ 1 ::分配器<PimplCyclic :: Cyclic :: Impl >>> allocate(unsigned long ,void const *)在/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:1813中
5:/Applications/Xcode.app/Contents中的std :: __ 1 :: shared_ptr <PimplCyclic :: Cyclic :: Impl> std :: __ 1 :: shared_ptr <PimplCyclic :: Cyclic :: Impl> :: make_shared <>()在/Applications/Xcode.app/Contents中/开发人员/工具链/XcodeDefault.xc工具链/ usr / include / c ++ / v1 /内存:4326
6:在_ZNSt3__1L11make_sharedIN11PimplCyclic6Cyclic4ImplEJEEENS_9enable_ifIXntsr8is_arrayIT_EE5valueENS_10shared_ptrIS5_EEE4typeEDpOT0_ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:4706
7:/Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28中的PimplCyclic :: Cyclic :: Cyclic()
8:/Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:29中的PimplCyclic :: Cyclic :: Cyclic()
9:主要位于/Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/main.cpphaps


看到Pimpl有点怀疑,其中的实现存储在std :: shared_ptr中。
基于原始指针的经典P​​impl过于陈旧,std :: unique_ptr的副作用是在立面上扩展了对复制语义的禁止。 这样的外观将实现唯一所有权的习惯用法,这可能与建筑理念不符。 通过使用std :: shared_ptr来存储实现,我们得出结论,该类旨在提供共享所有权。

这与经典的泄漏有何不同-通过显式调用new而无需后续删除来分配内存? 以同样的方式,界面和实现中的一切都会变得很漂亮-一个错误。
我们正在讨论射杀自己的现代方法。

反模式“僵尸”


因此,从以上材料可以明显看出:
-智能指针可以绑定到节点中;
-使用std :: enable_shared_from_this可以对此有所帮助,因为 允许类的实例在几乎没有外部帮助的情况下绑定到节点。

现在-注意-本文的关键问题:智能指针中包装的资源类型重要吗? RAII文件管理和异步HTTPS连接之间有区别吗?

简单僵尸


所有后续僵尸示例的通用代码已移至通用库。

具有适中名称Manager的抽象僵尸界面:

普通/ 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 


侦听器的抽象接口,准备接受线程安全的文本:

普通/ 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 


向控制台显示文本的侦听器。 从我的文章技巧中实现SingletonShared概念, 以避免调用Singleton时出现未定义的行为

通用/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 


通用/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 


最后,第一个僵尸,最简单也最巧妙。

简单的僵尸
 #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 


僵尸在单独的线程中运行lambda函数,并定期将字符串发送给侦听器。 用于工作的Lambda函数需要信号量和侦听器,它们是僵尸类的字段。 lambda函数不会将它们捕获为单独的字段,而是将对象用作聚合器。 在lambda函数完成之前销毁僵尸类的实例将导致未定义的行为。 为了避免这种情况,lambda函数捕获了shared_from_this()的强副本。
在僵尸析构函数中,将信号量设置为false,然后为流调用detach()。 设置信号量告诉线程关闭。

在析构函数中,有必要不调用detach(),而是调用join()!
...并获得一个无限期阻止执行的析构函数,这可能是不可接受的。

因此,这违反了RAII! RAII应该只在释放资源后才退出析构函数!
如果严格-如果是,则僵尸析构函数不会释放资源,而仅保证将进行释放 。 有时会产生-可能很快,或者可能不是真的。 甚至main可能更早完成工作-然后操作系统将强制清除线程。 但是实际上,“正确”和“错误”的RAII之间的界限可能很细:例如,“正确”的RAII在临时文件的析构函数中调用std :: filesystem :: remove()此时,写入命令仍将位于任何易失性高速缓存中并且不会被诚实地写入硬盘的磁板中。

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


控制台输出
SimpleZomby还活着!
SimpleZomby还活着!
SimpleZomby还活着!
SimpleZomby还活着!
SimpleZomby还活着!
==================================================== ===========
| 僵尸被杀|
==================================================== ===========
SimpleZomby还活着!
SimpleZomby还活着!
SimpleZomby还活着!
SimpleZomby还活着!
SimpleZomby还活着!


从程序输出中可以看到:
-僵尸即使离开了视野仍继续工作;
-没有为僵尸或WriteToConsoleListener调用析构函数。
发生内存泄漏。
资源泄漏。 在这种情况下,资源就是执行线程。
本应停止的代码继续在单独的线程中工作。
通过使用我的文章《 避免在调用Singleton时发生不确定的行为》中的SingletonWeak技术,可以防止WriteToConsoleListener泄漏,但我故意没有这样做。


(图片来自这里

为什么是僵尸?
因为他被杀了,他还活着。

这与前面的示例中的循环引用有何不同?
丢失的资源不仅是一块内存,而且还可以独立于启动线程的线程独立地执行代码。

有可能摧毁“僵尸”吗?
离开范围后(即销毁所有外部对僵尸的强弱引用)后,这是不可能的。 僵尸在决定自杀时会被摧毁(是的,这是一种活跃的行为),也许永远不会,即 直到应用程序终止时操作系统清除为止。 当然,用户代码可能会对退出僵尸代码的条件产生一些影响,但是这种影响将是间接的,并且取决于实现。

并且在离开范围之前?
您可以显式调用僵尸析构函数,但是由于智能指针析构函数也反复破坏对象,因此不太可能避免未定义的行为-这是与RAII的斗争。 或者,您可以添加显式取消初始化的功能-这是对RAII的拒绝。

这与仅在启动线程后再执行detach()有何不同?
在僵尸的情况下,与简单地调用detach()相比,有一种想法可以阻止流量。 只有它不起作用。 拥有正确的想法有助于掩盖问题。

这个例子仍然是合成的吗?
部分。 在这个简单的示例中,没有足够的理由使用shared_from_this()-例如,您可以通过捕获weak_from_this()或捕获类中的所有必需字段来进行操作。 但是随着任务的复杂性,平衡可能会转移到一边
shared_from_this()。

Valgrind,Valgrind! 我们还有针对僵尸的另一道防线!
las,嗯-但是Valgrind没有透露内存泄漏。 为什么-我不知道。 在诊断中,只有“可能丢失”的条目表示系统功能-与计算空的主电源时大约相同和大约相同的数量。 没有用户代码参考。 其他动态分析工具可能会做得更好,但是如果您仍然依靠它们,请继续阅读。

踩僵尸


此示例中的代码通过步骤resolveDnsName ---> connectTcp ---> EstablishmentSsl ---> sendHttpRequest ---> readHttpReply,模拟异步执行中客户端HTTPS连接的操作。 每个步骤大约需要一秒钟。

步进僵尸
 #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 


步进僵尸
 #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; } 


控制台输出
N13SteppingZomby5ZombyE :: resolveDnsName已启动
N13SteppingZomby5ZombyE :: resolveDnsName完成
N13SteppingZomby5ZombyE :: connectTcp已启动
==================================================== ===========
| 僵尸被杀|
==================================================== ===========
N13SteppingZomby5ZombyE :: connectTcp完成
N13SteppingZomby5ZombyE ::建立Ssl已启动
N13SteppingZomby5ZombyE ::建立SSL已完成
N13SteppingZomby5ZombyE :: sendHttpRequest已启动
N13SteppingZomby5ZombyE :: sendHttpRequest完成
N13SteppingZomby5ZombyE :: readHttp回复开始
N13SteppingZomby5ZombyE :: readHttp回复完成
N13SteppingZomby5ZombyE ::〜僵尸
N6Common22WriteToConsoleListenerE ::〜WriteToConsoleListener


与前面的示例一样,对runOnce()的调用导致了循环引用。
但是这一次,调用了Zomby和WriteToConsoleListener析构函数。 正确释放了所有资源,直到应用程序终止。 没有发生内存泄漏。

那是什么问题呢?
问题在于,僵尸的寿命太长了-在与僵尸的所有外部强弱环节都被摧毁后大约三分半秒。 比他应该活的时间长了三秒钟。 一直以来,他一直致力于促进HTTPS连接的实现-直到结束为止。 尽管不再需要结果。 尽管事实是卓越的业务逻辑试图阻止僵尸。

好吧,考虑一下,您会得到不需要的答案...。
在客户端HTTPS连接的情况下, 对我们这方面的后果可能如下:
-内存消耗;
-CPU消耗;
-TCP端口消耗;
-通信信道的带宽(请求和响应都可以是兆字节的卷);
-意外的数据可能会中断高层业务逻辑的操作-直到过渡到错误的执行分支或未定义的行为,因为 响应处理机制可能已经被破坏。
在远程方面 (请不要忘记-HTTPS请求是针对某人的)-完全相同的资源浪费,此外还可能:
-在公司网站上发布猫的照片;
-禁用厨房的地板采暖;
-在交易所执行交易指令;
-从您的帐户转帐;
-发射洲际弹道导弹。
业务逻辑试图通过删除所有与僵尸有关的强弱链接来阻止僵尸。 应该停止HTTPS请求的进度-还不算太晚,尚未发送应用程序级别的数据。
但是僵尸以自己的方式决定。

业务逻辑可以创建新对象来代替僵尸,然后再次尝试销毁它们,从而增加资源消耗。
在连续过程(例如Websocket连接)的情况下,资源浪费可能会持续数小时,并且如果断开连接时实现中存在自动重新连接机制,通常可以将其停止。

瓦尔格朗德?
没机会 一切都已正确释放并清理。 延迟不是来自主线程,而是完全正确的。

布兹德僵尸


本示例使用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?
过去 尽管似乎一直在检测泄漏。

野外的僵尸


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

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

:

stackoverflow ,
,


结论


, «».

, .

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/zh-CN471326/


All Articles