无锁堆栈:在pop()期间检查危险指针时的可见性问题?

时间:2016-12-22 22:24:21

标签: c++ multithreading c++11 lock-free memory-barriers

我正在阅读Anthony William的 C ++并发行动。第7章描述了开发无锁堆栈的过程,并说明了使无锁编程变得困难的常见问题。具体来说,第7.2.3节(检测无法使用危险指针回收的节点)描述了如何使用危险指针来避免数据竞争并确保其他线程不会被淘汰delete一个节点仍由另一个线程引用。

此代码是该章中所示的pop()的迭代之一:

std::shared_ptr<T> pop()
{
  std::atomic<void*>& hp = get_hazard_pointer_for_current_thread();
  node* old_head = head.load();

  do
  {
    node* temp;

    do
    {
      temp = old_head;
      hp.store(old_head);
      old_head = head.load();
    } while(old_head != temp);
  }
  while(old_head &&
    !head.compare_exchange_strong(old_head,old_head->next));

  hp.store(nullptr);
  std::shared_ptr<T> res;

  if(old_head)
  {
    res.swap(old_head->data);

    if(outstanding_hazard_pointers_for(old_head))
    {
      reclaim_later(old_head);
    }
    else
    {
      delete old_head;
    }

    delete_nodes_with_no_hazards();
  }

  return res;
}

我对这个片段有疑问:

    if(outstanding_hazard_pointers_for(old_head))
    {
      reclaim_later(old_head);
    }
    else
    {
      delete old_head;
    }

危险指针的目的是确保在没有其他线程可能仍在使用时删除old_headoutstanding_hazard_pointers_for的建议实施如下:

unsigned const max_hazard_pointers=100;
struct hazard_pointer
{
  std::atomic<std::thread::id> id;
  std::atomic<void*> pointer;
};
hazard_pointer hazard_pointers[max_hazard_pointers];

bool outstanding_hazard_pointers_for(void* p)
{
  for(unsigned i=0; i < max_hazard_pointers; ++i)
  {
    if(hazard_pointers[i].pointer.load() == p)
    {
      return true;
    }
  }

  return false;
}

基本上,扫描危险指针数组以检查指向节点的指针是否存在。我想知道为什么这个操作确实安全。执行原子load(),即使使用顺序一致的排序,load()也可能加载过时值。因此,可能找不到ppop()将删除仍在使用的节点。

想象一下发生以下情况:

  • 线程A开始执行pop()并在执行之前被抢占

    while(old_head &&
      !head.compare_exchange_strong(old_head,old_head->next));
    

    线程A因此将当前头部视为old_head,并将其保存到其危险指针中。当线程被唤醒并尝试弹出调用old_head的头部时,head.compare_exchange_strong(old_head, old_head->next)将被取消引用。

  • 线程B开始调用pop()向下

    if(outstanding_hazard_pointers_for(old_head))
    

    old_head将成为堆栈的当前头部,即线程A引用的同一节点old_head。如果线程A的危险指针上的delete old_head返回线程A存储的最新值,则线程B将 load()

基本上:我想知道线程B是否load()可以使用陈旧值而不是最新值。换句话说,我不确定为什么要返回线程A(old_node)设置的值。

这个推理的缺陷在哪里?在hp.store(old_head)之前,我找不到另一个线程上hazard_pointers[i].pointer.load()为什么会发生的理由。

2 个答案:

答案 0 :(得分:1)

我正在回答我自己的问题有两个原因:我认为我接受的答案不是很清楚,JJ15k's comment证实了这种印象。

基本上关键是观察另一个线程是否通过if(outstanding_hazard_pointers_for(old_head)) 看到在执行{{1}之前被抢占的另一个线程看到的old_head while(old_head && !head.compare_exchange_strong(old_head, old_head->next)) },它必须使用相同的head.compare_exchange_strong(old_head, old_head->next)执行old_head。但是(假设<表示发生之前的关系):

thread A: hp.store(old_head)     < 
thread A: old_head = head.load() < 
thread B: head.compare_exchange_strong(old_head, old_head->next)

请记住,线程B看到我们在第一条指令中加载的相同的 old_head,它将其值交换为old_head->next。我们仍然在head.load()中看到相同的值,这就是线程A hp.store(old_head)发生在线程B compare_exchange_strong之前的原因。

因此,即将检查危险指针中包含的头部是否可以删除的线程以查看old_head。还要注意old_head = head.load()所起的基本作用(以及包含那些看起来多余的语句的循环)。如果没有load操作,store old_headhpcompare_exchange_strong之间就不存在关系。

我希望这能回答你的问题。

答案 1 :(得分:0)

我对代码的理解如下。

如果另一个线程中的hp.store(old_head)未发生 - 在此线程中调用hazard_pointers[i].pointer.load()之前,则表示此线程已成功执行head.compare_exchange_strong(old_head,old_head->next)调用。这意味着对于另一个线程old_head != temp,它将执行另一次尝试将正确的old_head存储为线程的hp

这意味着可以安全地删除当前线程中的原始old_head指针,因为它实际上并没有被其他线程使用。