无锁队列算法,重复读取一致性

时间:2010-10-06 14:41:35

标签: data-structures concurrency nonblocking

我正在研究lock-free (en-,de-)queue algorithms of Michael and Scott。问题是我无法解释/理解(除了代码本身的评论之外,论文也没有这样做)。

入队:

  enqueue(Q: pointer to queue_t, value: data type)
   E1:   node = new_node()        // Allocate a new node from the free list
   E2:   node->value = value      // Copy enqueued value into node
   E3:   node->next.ptr = NULL    // Set next pointer of node to NULL
   E4:   loop                     // Keep trying until Enqueue is done
   E5:      tail = Q->Tail        // Read Tail.ptr and Tail.count together
   E6:      next = tail.ptr->next // Read next ptr and count fields together
   E7:      if tail == Q->Tail    // Are tail and next consistent?
               // Was Tail pointing to the last node?
   E8:         if next.ptr == NULL
                  // Try to link node at the end of the linked list
   E9:            if CAS(&tail.ptr->next, next, <node, next.count+1>)
  E10:               break        // Enqueue is done.  Exit loop
  E11:            endif
  E12:         else               // Tail was not pointing to the last node
                  // Try to swing Tail to the next node
  E13:            CAS(&Q->Tail, tail, <next.ptr, tail.count+1>)
  E14:         endif
  E15:      endif
  E16:   endloop
         // Enqueue is done.  Try to swing Tail to the inserted node
  E17:   CAS(&Q->Tail, tail, <node, tail.count+1>)

为什么需要E7?正确性取决于它吗?或者它只是一个优化?如果另一个线程在第一个线程执行了E5但尚未执行E7时成功执行了E17或D10,(并且更改了Q-> Tail),则此if可能会失败。但是如果在第一个线程执行E7之后立即执行E17会怎样?

编辑:这最后一句是否证明E7不能超过优化?我的直觉是它确实,因为我给出一个场景“显然”if语句会做出错误的决定,但算法仍然应该正常工作。但是,我们可以用随机条件替换if的条件,而不会影响正确性。这个论点中有什么漏洞吗?

出列:

dequeue(Q: pointer to queue_t, pvalue: pointer to data type): boolean
   D1:   loop                          // Keep trying until Dequeue is done
   D2:      head = Q->Head             // Read Head
   D3:      tail = Q->Tail             // Read Tail
   D4:      next = head.ptr->next      // Read Head.ptr->next
   D5:      if head == Q->Head         // Are head, tail, and next consistent?
   D6:         if head.ptr == tail.ptr // Is queue empty or Tail falling behind?
   D7:            if next.ptr == NULL  // Is queue empty?
   D8:               return FALSE      // Queue is empty, couldn't dequeue
   D9:            endif
                  // Tail is falling behind.  Try to advance it
  D10:            CAS(&Q->Tail, tail, <next.ptr, tail.count+1>)
  D11:         else                    // No need to deal with Tail
                  // Read value before CAS
                  // Otherwise, another dequeue might free the next node
  D12:            *pvalue = next.ptr->value
                  // Try to swing Head to the next node
  D13:            if CAS(&Q->Head, head, <next.ptr, head.count+1>)
  D14:               break             // Dequeue is done.  Exit loop
  D15:            endif
  D16:         endif
  D17:      endif
  D18:   endloop
  D19:   free(head.ptr)                // It is safe now to free the old node
  D20:   return TRUE                   // Queue was not empty, dequeue succeeded

再次,为什么需要D5?正确还是优化?我不确定这些测试给出的“一致性”是什么,因为在if成功之后它们似乎会变得不一致。

这看起来像是一种标准技术。有人可以解释它背后的动机吗?对我来说,似乎意图避免做一个(昂贵的)CAS,在少数情况下可以注意到它肯定会失败,但代价是总是做一个额外的读取,这不应该是这么多本身更便宜(例如在Java中,Q->Tail将需要是易失性的,所以我们知道我们不仅仅是读取寄存器中缓存的副本而是读取真实的东西,这将被翻译成在读取前加上某种围栏),所以我不确定这里到底发生了什么......谢谢。

编辑这已移植到Java,更确切地说是移植到ConcurrentLinkedQueue,例如E7是第194行,而D5是第212行。

2 个答案:

答案 0 :(得分:4)

我被困在同一个问题上,并怀疑这可能是一个优化,所以我问Maged Michael,他是本文的作者之一。这是他的回答:

  

正确性需要E7和D5。

     

以下案例说明了为什么需要E7:

     
      
  • 线程P从第E5行的Q-&gt;尾部读取值<A,num1>

  •   
  • 其他线程更改队列,以便删除节点A,以后可以在不同的队列(或具有相似节点结构的不同结构)中重用,或者由线程分配以将其插入此队列中   相同的队列。在任何情况下,A都不在此队列中,并且其下一个字段具有   值<NULL, num2>

  •   
  • 在第E6行,P从A->接下来读取值<NULL, num2>

  •   
  • (跳过第E7行)

  •   
  • 在E8行中,P找到next.ptr == NULL

  •   
  • 在第E9行,P在A-&gt;下执行成功CAS,因为它找到A->next == <NULL, num2>并将其设置为<node,num2+1>

  •   
  • 现在,在A之后错误地插入了新节点,该节点不属于此队列。这也可能破坏另一个无关的   结构

  •   
     

对于E7行,P会发现Q-> Tail已经改变了   本来可以重新开始。

     

同样适用于D5。

基本上,如果我们从tail.ptr->next的读取将使我们相信下一个指针为空(因此我们可能写入节点),我们必须仔细检查此null指的是当前队列的结尾。如果在我们读取null之后节点仍然在队列中,我们可以假设它确实是队列的结束,并且比较和交换将(给定计数器)捕获该节点发生任何事情的情况 E7中进行测试后(从队列中删除节点必然会改变其下一个指针)。

答案 1 :(得分:-1)

  

为什么需要E7?

它更适合优化。

考虑两个线程试图同时入队。他们都进入E5,但在线程1到达E7之前,线程2成功排队。当线程1到达E7时,它将观察到t == tail为false然后重试。这将避免昂贵的CAS。当然它不是完整的证据,因为E7可以在线程2排队之前成功并最终使CAS失败并且无论如何都必须重试。

  

为什么需要D5

与D5相似

同样,没有E7和D5的两个功能都可以工作。可能有一些基准测试正在进行,并发现在温和的争用下,双重检查会增加吞吐量(这更像是观察而不是事实)。

编辑:

我去看了一下这个队列上的论文。检查也是为了确保无锁算法的正确性,并减少数据结构的状态。

  

无锁算法是非阻塞的,因为如果有的话   尝试执行操作的非延迟进程   在队列中,保证操作完成   有限的时间。   只有当条件符合行时,enqueue操作才会循环   E7失败,E8行的条件失败,或比较   和E9行交换失败。出列操作循环   只有当D5行中的条件失败时,条件才符合   D6保持(并且队列不为空)或比较   和D13行交换失败。   我们通过显示表明该算法是非阻塞的   只有当一个过程循环超过有限次数时   另一个进程完成对队列的操作。

http://www.cs.rochester.edu/u/scott/papers/1996_PODC_queues.pdf