std::shared_ptr 和 std::experimental::atomic_shared_ptr 有什么区别?

What is the difference between std::shared_ptr and std::experimental::atomic_shared_ptr?

我阅读了 Antony Williamsfollowing 文章,据我所知,除了 std::shared_ptrstd::experimental::atomic_shared_ptr 中的原子共享计数指向共享对象的实际指针也是原子的?

但是当我读到 Antony 关于 C++ Concurrency 的书中描述的 lock_free_stack 的引用计数版本时,对我来说似乎同样适用于 std::shared_ptr,因为像 std::atomic_loadstd::atomic_compare_exchnage_weak 应用于 std::shared_ptr 的实例。

template <class T>
class lock_free_stack
{
public:
  void push(const T& data)
  {
    const std::shared_ptr<node> new_node = std::make_shared<node>(data);
    new_node->next = std::atomic_load(&head_);
    while (!std::atomic_compare_exchange_weak(&head_, &new_node->next, new_node));
  }

  std::shared_ptr<T> pop()
  {
    std::shared_ptr<node> old_head = std::atomic_load(&head_);
    while(old_head &&
          !std::atomic_compare_exchange_weak(&head_, &old_head, old_head->next));
    return old_head ? old_head->data : std::shared_ptr<T>();
  }

private:
  struct node
  {
    std::shared_ptr<T> data;
    std::shared_ptr<node> next;

    node(const T& data_) : data(std::make_shared<T>(data_)) {}
  };

private:
  std::shared_ptr<node> head_;
};

这两种类型的智能指针之间的确切区别是什么,如果 std::shared_ptr 实例中的指针不是原子的,为什么上面的无锁堆栈实现是可能的?

shared_ptr 上调用 std::atomic_load()std::atomic_compare_exchange_weak() 在功能上等同于调用 atomic_shared_ptr::load()atomic_shared_ptr::atomic_compare_exchange_weak()。两者之间应该没有任何性能差异。在 atomic_shared_ptr 上调用 std::atomic_load()std::atomic_compare_exchange_weak() 在语法上是多余的,可能会也可能不会导致性能下降。

shared_ptr中的原子"thing"并不是共享指针本身,而是它指向的控制块。这意味着只要您不跨多个线程改变 shared_ptr ,就可以了。请注意 copying a shared_ptr 只会改变控制块,而不是 shared_ptr 本身。

std::shared_ptr<int> ptr = std::make_shared<int>(4);
for (auto i =0;i<10;i++){
   std::thread([ptr]{ auto copy = ptr; }).detach(); //ok, only mutates the control block 
}

改变共享指针本身,例如从多个线程为其分配不同的值,是一种数据竞争,例如:

std::shared_ptr<int> ptr = std::make_shared<int>(4);
std::thread threadA([&ptr]{
   ptr = std::make_shared<int>(10);
});
std::thread threadB([&ptr]{
   ptr = std::make_shared<int>(20);
});    

在这里,我们改变了控制块(没关系),也改变了共享指针本身,使其指向来自多个线程的不同值。这不行。

这个问题的一个解决方案是用锁包裹shared_ptr,但是这个解决方案在某些争用下不是那么可扩展,并且在某种意义上,失去了标准共享指针的自动感觉。

另一种解决方案是使用您引用的标准函数,例如std::atomic_compare_exchange_weak。这使得同步共享指针的工作成为我们不喜欢的手动工作。

这就是原子共享指针发挥作用的地方。您可以改变来自多个线程的共享指针,而不必担心数据竞争,也无需使用任何锁。独立功能将是成员功能,它们的使用对用户来说将更加自然。这种指针对于无锁数据结构非常有用。

atomic_shared_ptr is an API refinement. shared_ptr already supports atomic operations, but only when using the appropriate atomic non-member functions。这是容易出错的,因为非原子操作仍然可用并且很容易被粗心的程序员意外调用。 atomic_shared_ptr 不易出错,因为它不会公开任何非原子操作。

shared_ptratomic_shared_ptr 公开了不同的 API,但它们不一定需要以不同的方式实现; shared_ptr 已经支持 atomic_shared_ptr 公开的所有操作。话虽如此,shared_ptr 的原子操作并没有达到应有的效率,因为它还必须支持非原子操作。因此,atomic_shared_ptr 可能会以不同的方式实现,这是出于性能方面的原因。这与单一职责原则有关。 "An entity with several disparate purposes... often offers crippled interfaces for any of its specific purposes because the partial overlap among various areas of functionality blurs the vision needed for crisply implementing each."(Sutter & Alexandrescu 2005,C++ 编码标准

N4162(pdf),原子智能指针的提案,有很好的解释。以下是相关部分的引述:

Consistency. As far as I know, the [util.smartptr.shared.atomic] functions are the only atomic operations in the standard that are not available via an atomic type. And for all types besides shared_ptr, we teach programmers to use atomic types in C++, not atomic_* C-style functions. And that’s in part because of...

Correctness. Using the free functions makes code error-prone and racy by default. It is far superior to write atomic once on the variable declaration itself and know all accesses will be atomic, instead of having to remember to use the atomic_* operation on every use of the object, even apparently-plain reads. The latter style is error-prone; for example, “doing it wrong” means simply writing whitespace (e.g., head instead of atomic_load(&head) ), so that in this style every use of the variable is “wrong by default.” If you forget to write the atomic_* call in even one place, your code will still successfully compile without any errors or warnings, it will “appear to work” including likely pass most testing, but will still contain a silent race with undefined behavior that usually surfaces as intermittent hard-to-reproduce failures, often/usually in the field, and I expect also in some cases exploitable vulnerabilities. These classes of errors are eliminated by simply declaring the variable atomic, because then it’s safe by default and to write the same set of bugs requires explicit non-whitespace code (sometimes explicit memory_order_* arguments, and usually reinterpret_casting).

Performance. atomic_shared_ptr<> as a distinct type has an important efficiency advantage over the functions in [util.smartptr.shared.atomic] — it can simply store an additional atomic_flag (or similar) for the internal spinlock as usual for atomic<bigstruct>. In contrast, the existing standalone functions are required to be usable on any arbitrary shared_ptr object, even though the vast majority of shared_ptrs will never be used atomically. This makes the free functions inherently less efficient; for example, the implementation could require every shared_ptr to carry the overhead of an internal spinlock variable (better concurrency, but significant overhead per shared_ptr), or else the library must maintain a lookaside data structure to store the extra information for shared_ptrs that are actually used atomically, or (worst and apparently common in practice) the library must use a global spinlock.