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

时间:2016-10-24 16:54:19

标签: c++ c++11 concurrency atomic smart-pointers

我阅读了 Antony Williams following文章,除了std::shared_ptrstd::experimental::atomic_shared_ptr中{1}}中的原子共享计数之外我还明白了共享对象也是原子的?

但是,当我读到安东尼的书中关于C++ Concurrency所描述的lock_free_stack的引用计数版本时,对我来说似乎std::shared_ptr同样适用,因为像std::atomic_load这样的函数},std::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实例中的指针不是原子的,为什么上面的无锁堆栈实现成为可能?

4 个答案:

答案 0 :(得分:19)

shared_ptr中的原子“事物”不是共享指针本身,而是它指向的控制块。意思是只要你不在多个线程中改变shared_ptr,你就可以了。请注意复制一个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。这使得同步共享指针的工作成为手动的,我们不喜欢这样做。

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

答案 1 :(得分:6)

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

  

<强>一致性即可。据我所知,[util.smartptr.shared.atomic]   函数是标准中唯一的原子操作   不能通过atomic类型获得。适用于所有类型   除shared_ptr之外,我们教程序员使用原子类型   在C ++中,不是atomic_* C风格的函数。这部分是因为......

     

<强>正确性即可。使用自由函数会使代码容易出错   而且默认情况下是活泼的。写入atomic一次就好了   变量声明本身并知道所有访问   将是原子的,而不是必须记住使用atomic_*   对对象的每次使用进行操作,即使是显而易见的读取。   后一种风格容易出错;例如,“做错了”意味着   只是写空格(例如,head而不是atomic_load(&head)),   所以在这种风格中,变量的每次使用都是“默认错误的。”如果你忘记了   即使在一个地方写atomic_*电话,您的代码仍然可以   成功编译没有任何错误或警告,它将“出现   工作“包括可能通过大多数测试,但仍将包含一个   具有未定义行为的沉默种族通常表现为间歇性的   难以重现的故障,经常/通常在现场,   我预计在某些情况下也会出现可利用的漏洞。   只需声明变量atomic即可消除这些类错误,   因为那时它默认是安全的并且写同一组   错误需要显式的非空白代码(有时是显式的)   memory_order_*个参数,通常是reinterpret_cast ing。。

     

<强>性能即可。 atomic_shared_ptr<>作为一种独特的类型   具有重要的效率优势   [util.smartptr.shared.atomic]中的函数 - 它可以简单地存储一个   内部自旋锁的附加atomic_flag(或类似内容)   和atomic<bigstruct>一样。相比之下,现有的独立功能   必须在任意shared_ptr上使用   对象,即使绝大多数shared_ptr都会   从不以原子方式使用。这使得自由功能固有地发挥作用   效率低下;例如,实施可能需要   每shared_ptr携带一个内部自旋锁的开销   变量(更好的并发性,但每个的开销很大)   shared_ptr),否则库必须保留旁视数据   用于存储shared_ptr的额外信息的结构   实际上是原子地使用,或者(最差的,显然是常见的)   实践)图书馆必须使用全球自旋锁。

答案 2 :(得分:5)

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

答案 3 :(得分:4)

atomic_shared_ptr是一项API细化。 shared_ptr已经支持原子操作,但仅在使用适当的atomic non-member functions时才支持。这很容易出错,因为非原子操作仍然可用,并且对于不小心的程序员来说太容易被意外调用。 atomic_shared_ptr不易出错,因为它不会暴露任何非原子操作。

shared_ptratomic_shared_ptr公开了不同的API,但它们不一定需要以不同的方式实施; shared_ptr已支持atomic_shared_ptr公开的所有操作。话虽如此,shared_ptr的原子操作并不像它们那样有效,因为它还必须支持非原子操作。因此,性能原因可能导致atomic_shared_ptr的实现方式不同。这与单一责任原则有关。 &#34;具有多个不同目的的实体......通常为其任何特定目的提供残缺的界面,因为各个功能区域之间的部分重叠模糊了清晰实施每个领域所需的愿景。&#34; (Sutter&amp; Alexandrescu 2005, C ++编码标准