在书里C++ 并发实践 https://www.manning.com/books/c-plus-plus-concurrency-in-action,作者举了一个使用hazard指针实现无锁栈数据结构的例子。部分代码如下:
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
节点受应用行为分析问题 https://en.wikipedia.org/wiki/ABA_problem。即使价值head
保持不变,它原来指向的节点可能已经被删除了。一个新的head
分配节点,该节点恰好具有与前一个节点相同的地址值。
默认memory_order http://en.cppreference.com/w/cpp/atomic/memory_order for load()
操作是std::memory_order_seq_cst
,确保所有操作的顺序一致性(全局总排序):
Each 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
。这个记忆顺序给你一个acquire负载和操作release存储操作,防止在同一线程内重新排序。因此,“成功”读作(old_head==temp
)保证保存正确的数据。
将这两个负载视为同步点 - 因为它们执行acquire操作时,它们与各自同步release修改这些值的操作,导致所有写入变得可见。
您所描述的问题不会以任何方式损害该示例。pop()
函数的目的是删除顶部元素,它会做到这一点。如果同时添加/删除元素,它将弹出该元素,无论其地址是什么(甚至可能与之前获取的地址相同)。这是一个完全不同的问题。考虑:
concurrent_stack<int> p;
if (!p.empty() && (p.top() == 5))
{
auto t = p.pop();
assert( t ); // May fail
assert( *t == 5 ); // May fail
}
两个断言都可能失败,如果许多线程非常密集地使用堆栈,很可能会经常失败。但这并不是由于错误执行pop()
,但事实上,您需要更强的访问限制来确保最后检查的元素确实从堆栈中删除。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)