我正在研究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行。
答案 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