这个危险指针示例是否因 ABA 问题而存在缺陷?

2023-12-29

在书里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(使用前将#替换为@)

这个危险指针示例是否因 ABA 问题而存在缺陷? 的相关文章

随机推荐