当前位置: 华文星空 > 知识

老板不让用shared_ptr,会是什么原因?

2015-07-21知识

std::shared_ptr目前发现一些 实现定义 所存在的问题,由于不同编译器的实现方法大不相同,因此这里只指出部分编译器可能存在的问题和坑以及解决方案,不讨论语言标准本身的设定。至于这些问题是不是问题,见仁见智。

问题:

  1. 多线程环境下,构造析构的实现并非原子,只有引用计数是安全的,这在都使用std::shared_ptr时没有问题,然而多线程环境由于异步任务琐碎且分布于不同线程,出现循环引用的可能较大,std::weak_ptr就容易出现线程安全事故(修正一下,新的标准中已支持多线程安全的lock)。atomic_shared_ptr存在的主要问题则是CAS自旋锁失败后没有及时让出时间片,伪代码:

while ( CAS (...)); // Bad while ( CAS (...)){ yield ();} // Good

CAS放在一个死循环中是非常不好的实践,对性能可能会造成毁灭性打击,在高实时性要求的系统中,是不定时的性能炸弹。

2. std::shared_ptr逻辑可能全部在模板中实现,而其本身代码量又很重,这会导致编译速度被拖慢,且包体大小夸张。

3. 内部计数器需要多一个额外的内存分配,默认分配器下会造成内存碎片和性能问题。

4. 无法设置默认析构方法,不容易开洞:如项目中为了跨动态可执行单元,可能会需要抽象析构函数为一个虚函数(详见COM中Release的设定方法),因此我们可能需要智能指针的deleter有一个默认的逻辑:

template<typename T> void Deleter(T* ptr) { if constexpr (std::is_base_of_v<IReleasable, T>) { ptr->Release(); } else { delete ptr; } }

虽然可以自己定义deleter,但这在多人协作项目中远不如默认方法方便。

解决方案:

  1. 在引用计数和析构模块,可使用自定义的自旋锁实现,关于自旋锁在不同平台的最佳实践,Stack Overflow有大量文献,方法虽然不一,但大体思路一致,都是尽可能让排队的线程不会疲于死循环。
  2. 对于智能指针来说,指针的类型并不重要,只需要一个void*和一个析构函数的函数指针即可,因此智能指针中重量级的逻辑都可以单独实现并放到某个固定的编译单元而非header,对于多继承的类型转换偏移可以直接记录为整数,这就解决了编译速度和包体问题。
  3. 承接第二条,既然逻辑写入到独立的编译单元,就可以用一个静态的分配池来分配大小和类型一致的计数器。
  4. 自己造轮子,开洞美滋滋。

最后附上自己的实现: