内存安全性#
又到了关于如何让C++别崩溃的话题。没办法啊必须要仔细研究一下才行
freekill-asio主要基于std+asio库开发。其中内存安全与线程安全需要特别留意关心。
线程安全#
在游戏中最最常见的多线程场景如图。当调用 asio::post 时,问题转为异步安全,
当直接调用某某方法时,需要探讨那个方法的线程安全性。
异步内存安全#
freekill-asio采用的内存管理策略:
基于shared_ptr+weak_ptr的方式处理内存安全,尽量避免悬垂指针的问题。
规定动态分配的类型必须在主线程完成构造和析构。
但是实际操练下来,疏漏的点还是不少。 主要容易出问题的点集中在主线程处理异步callback上,这个经常与对象生命周期相关, 处理不慎会出现use-after-free问题(主要是捕获的this指针变成悬垂指针了)。
io_ctx.run() 差不多可以比照Qt的事件循环理解,
图中会触发异步回调的事件很多,稍微列举一些:
Socket有新数据可读
Socket读取到EOF(也就是断开连接)
Acceptor收到新连接请求
asio::steady_timer时间到了某处调用了
asio::post以及其他
要明白异步调用带来的内存问题,我们首先要明确Functor的概念。Functor是一个匿名类, 它重载了`operator()`,并将“捕获”到的参数作为类的私有成员,返回这个类的实例。 这个捕获变量自然是通过拷贝的方式,再加上我们根本无法预测异步回调的执行顺序,因此注意事项有:
若捕获了
std::shared_ptr,那个指针的引用计数会随之+1 - 这会导致几个用来销毁对象的函数失效,对象因为这个引用没有释放 - 也可能导致对象在意料之外的情境下调用析构函数若捕获了
this,无法保证执行时this是否还有效(除非是那几个单例)若捕获了
std::string_view,那么执行到的时候它指向的data基本已经被释放了
例如:
asio::post([this](){ this->func(100); });
这个函数不会在主线程立刻执行,主线程甚至可能执行了某个会析构掉this的回调,再来调用上面这个。
这样会导致访问悬空的this导致程序崩溃(有时候不会崩溃,魅力C++)。
很遗憾 std::bind 和lambda都存在这个问题,毕竟this是个裸指针,本身不提供内存管理辅助。
解决方案一般是引入weak_ptr,在使用this之前先检查内存是否被释放。上面的代码一般会被改成:
asio::post([this, weak = weak_from_this()](){
if (weak->lock()) {
this->func(100);
}
});
这样既不会因为lambda的存在延长this的生命周期,又能做到检测。
但是如果在另一个线程调用 weak->lock() 的话,若成功则返回 std::shared_ptr ,
若对生命周期管理不当可能会在weak产出的指针引用计数减一之前主线程先把对象销了,
导致析构函数执行在主线程之外的线程。
其他注意点#
使用STL容器时,要尤其注意迭代器失效的问题。当遍历某个容器的时候, 循环体中的代码不能调用到任何可能导致修改这个容器的方法。
注意方法的语义。写的是什么名字就执行什么功能,不要隐含的搞特殊。
项目中实际的处理#
以上属于穿插着项目实际叙述了一些安全的注意点,接下来的文章结合具体类详细说明它们的处理逻辑。 只能说还好freekill-asio也就只是个5000行左右的小服务器项目,至少核心逻辑目前是这么点量级。