内存安全性
============

又到了关于如何让C++别崩溃的话题。没办法啊必须要仔细研究一下才行

freekill-asio主要基于std+asio库开发。其中内存安全与线程安全需要特别留意关心。

线程安全
--------------

在游戏中最最常见的多线程场景如图。当调用 ``asio::post`` 时，问题转为异步安全，
当直接调用某某方法时，需要探讨那个方法的线程安全性。

.. uml:: uml/thread-signal1.puml

异步内存安全
--------------

freekill-asio采用的内存管理策略：

- 基于shared_ptr+weak_ptr的方式处理内存安全，尽量避免悬垂指针的问题。
- 规定动态分配的类型必须在主线程完成构造和析构。

但是实际操练下来，疏漏的点还是不少。
主要容易出问题的点集中在主线程处理异步callback上，这个经常与对象生命周期相关，
处理不慎会出现use-after-free问题（主要是捕获的this指针变成悬垂指针了）。

.. uml:: uml/async.puml

``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基本已经被释放了

例如：

.. code:: cpp

   asio::post([this](){ this->func(100); });

这个函数不会在主线程立刻执行，主线程甚至可能执行了某个会析构掉this的回调，再来调用上面这个。
这样会导致访问悬空的this导致程序崩溃（有时候不会崩溃，魅力C++）。
很遗憾 ``std::bind`` 和lambda都存在这个问题，毕竟this是个裸指针，本身不提供内存管理辅助。

解决方案一般是引入weak_ptr，在使用this之前先检查内存是否被释放。上面的代码一般会被改成：

.. code:: cpp

   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行左右的小服务器项目，至少核心逻辑目前是这么点量级。
