我阅读了 Antony Williams 的following文章,除了std::shared_ptr
中std::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
实例中的指针不是原子的,为什么上面的无锁堆栈实现成为可能?
答案 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_ptr
和atomic_shared_ptr
公开了不同的API,但它们不一定需要以不同的方式实施; shared_ptr
已支持atomic_shared_ptr
公开的所有操作。话虽如此,shared_ptr
的原子操作并不像它们那样有效,因为它还必须支持非原子操作。因此,性能原因可能导致atomic_shared_ptr
的实现方式不同。这与单一责任原则有关。 &#34;具有多个不同目的的实体......通常为其任何特定目的提供残缺的界面,因为各个功能区域之间的部分重叠模糊了清晰实施每个领域所需的愿景。&#34; (Sutter&amp; Alexandrescu 2005, C ++编码标准)