在书C++ Concurrency in Action中,作者举了一个使用危险指针来实现无锁堆栈数据结构的例子。部分代码如下:
std::shared_ptr<T> pop()
{
std::atomic<void*>& hp=get_hazard_pointer_for_current_thread();
node* old_head=head.load();
node* temp;
do
{
temp=old_head;
hp.store(old_head);
old_head=head.load();
} while(old_head!=temp);
// ...
}
描述说
您必须在
while
循环中执行此操作,以确保node
没有 在读取旧head
指针和之后删除了 设置危险指针。在这个窗口期间没有其他线程 知道你正在访问这个特定的节点。幸运的是,如果老了head
节点将被删除,head
本身必须已更改, 所以你可以检查这个并继续循环,直到你知道head
指针仍然具有您设置危险指针的相同值。
我认为代码存在缺陷,因为head
节点受ABA problem的约束。即使head
的值保持不变,它最初指向的节点也可能已被删除。分配了一个新的head
节点,该节点恰好具有与前一个节点相同的地址值。
答案 0 :(得分:5)
load()
操作的默认memory_order
为std::memory_order_seq_cst
,可确保所有操作的顺序一致性(全局排序总数):
从原子变量加载的每个
memory_order_seq_cst
操作B
M
,观察以下其中一项:
- 修改
A
的上一次操作M
的结果,该操作在单个总订单中显示在B
之前- 或者,如果有这样的
之前A
,B
可能会在M
memory_order_seq_cst
上观察到某些修改的结果,而不是A
发生在A
- 或者,如果没有这样的
B
,M
可能会观察到memory_order_seq_cst
std::memory_order_seq_cst
的{{1}}的某些无关修改的结果。
因此,如果节点被修改(删除)并且这发生在全局全局顺序中的第二次读取之前,则可以保证看到该更改,因此循环将继续执行。如果在此之后订购此修改,则因为已经设置了危险指针,所以没有任何危害。
您有此保证,因为存储到危险指针也是使用old_head==temp
完成的。此内存顺序为存储提供获取操作以及 release 操作,从而防止在同一线程内重新排序。因此,“成功”读取(pop()
)可确保保存正确的数据。
将这两个加载视为同步点 - 因为它们执行获取操作,它们与相应的 release 操作同步,这些操作会修改这些值,导致所有写入变为可见。
您描述的问题不会以任何方式破坏该示例。 concurrent_stack<int> p;
if (!p.empty() && (p.top() == 5))
{
auto t = p.pop();
assert( t ); // May fail
assert( *t == 5 ); // May fail
}
函数旨在删除顶部元素,它将执行此操作。如果在此期间添加/删除元素,它将弹出它,无论它的地址是什么(它甚至可能与先前获取的地址相同)。这是一个完全不同的问题。考虑:
pop()
两个断言都可能失败,如果许多线程非常密集地使用堆栈,很可能会经常失败。但这不是由于=SUM(IF(FREQUENCY(IF(B2:B10=C1,IF(A2:A10<>"",MATCH(A2:A10,A2:A10,0))),ROW(A2:A10)-ROW(A2)+1),1))
的错误实现,而是因为您需要更强的访问限制以确保最后一个已检查的元素确实从堆栈中删除。