原子智能指针

std::shared_ptr由控制块和相关资源组成。std::shared_ptr能够保证控制块是线程安全的,但是对相关资源的访问就不是了。这意味着,修改引用计数器是一个原子操作,可以确保资源删除一次。

线程安全的重要性

这里只说明std::shared_ptr具有定义良好的多线程语义是有多么重要。乍一看,使用std::shared_ptr并不是多线程程序的明智选择。根据定义,它是共享和可变的,是数据竞争和未定义行为的理想对象。另一方面,现代C++中有一条准则:不要接触内存。这意味着在多线程程序中,要尽可能使用智能指针。

关于原子智能指针的N4162提议,直接解决了当前智能指针实现的缺陷。这些缺陷可以归结为以下三点:一致性、正确性和高效性。下面将概述这三点,详系内容可参见提案N4162。

  • 一致性:std::shared_ptr对非原子数据类型,只能进行原子操作。
  • 正确性:因为正确的使用方式是基于严格的规则,所以使用全局性的原子操作非常容易出错。很容易忘记使用原子操作——例如,使用ptr = localPtr代替std::atomic_store(&ptr, localPtr)。由于数据竞争,结果是未定义的。如果使用原子智能指针,系统将不允许数据竞争的出现。
  • 高效性:与atomic_*函数相比,原子智能指针有很大的优势。原子版本是为特殊用例设计的,可以在内部使用std::atomic_flag作为一种低开销的自旋锁。如果将指针函数的非原子版设计为线程安全的,并用于单线程场景,那就太大材小用了,并且还会受到性能上的惩罚。

对我来说,正确性是最重要的。为什么?答案就在提案中。这个建议提供了一个线程安全的单链表,它支持插入、删除和搜索元素,并且这个单链表以无锁的方式实现。

线程安全的单链表

原子智能指针 - 图1

需要使用C++11编译器编译的地方都用红色标记。这个链表,使用原子智能指针实现要容易得多,也不容易出错。C++20的类型系统不允许在原子智能指针上使用非原子操作。

N4162提议将std::atomic_shared_ptrstd::atomic_weak_ptr作为原子智能指针。将它们合并到主流的ISO C++标准中,就变成了std::atomic: std::atomicstd::shared_ptr<T>std::atomicstd::weak_ptr<T>偏特化模板。

因此,std::shared_ptr的原子操作在C++20中是废弃的。