解释 Michael & Scott 无锁队列算法
Explain Michael & Scott lock-free queue alorigthm
我正在研究 Michael & Scott 的无锁队列算法并尝试用 C++ 实现它。
但是我在我的代码中产生了一个竞争,我认为算法中可能存在一个竞争。
我在这里阅读了论文:
简单、快速、实用的非阻塞和阻塞
并发队列算法
而原来的Dequeue伪代码如下:
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
在我看来,比赛是这样的:
- 线程 1 前进到 D3,然后停止。
- 线程2进阶到D3,读取与线程1相同的头
- 线程 2 幸运地一直推进到 D20,在 D19 它释放了 head.ptr
- 线程 1 继续并前进到 D4,尝试读取
head.ptr->next
,但由于 head.ptr
已被线程 1 释放,因此发生崩溃。
而且我的 C++ 代码确实总是在线程 1 的 D4 处崩溃。
谁能指出我的错误并给出一些解释?
谢谢,非常有趣的主题!它看起来确实像一个错误,但该论文的一位作者断言他们的 free() 不是我们都使用的普通 free(),而是一些神奇的 free(),所以没有错误。太棒了。
希望没有人在没有彻底分析的情况下将其投入生产。
这实际上是自 MS 队列的作者之一 Maged Michael 引入 Hazard Pointers [1] 以来一直在探索的非阻塞内存回收问题。
危险指针允许线程保留块,以便其他线程在它完成之前不会真正回收它们。但是,这种机制会带来不小的性能开销。
还有许多基于 epoch 的回收变体,例如 RCU [2,3],以及最近的基于间隔的回收 (IBR) [4]。它们通过保留纪元来避免 use-after-free,并且比 Hazard Pointers 更快。据我所知,基于epoch的回收被广泛采用来处理这个问题。
您可以查看下面提到的这些论文以了解更多详细信息。 Interval-Based Memory Reclamation 的论文讨论了很多背景知识。
这是非阻塞数据结构中的普遍问题,我们一般不会将其视为数据结构本身的错误---毕竟它只出现在使用手动内存管理的语言中,如C/C++ 但不是像 Java 这样的(顺便说一句,Michael & Scott Queue 已经在 Java Concurrency 中被采用多年)。
参考:
[1] 危险提示:无锁对象的安全内存回收,Maged M. Michael,IEEE Transactions on Parallel and Distributed Systems,2004。
[2]
无锁同步的内存回收性能,Thomas E. Hart 等人,并行和分布式计算杂志,2007 年。
[3]
阅读副本更新,Paul E. McKenney 等人,渥太华 Linux 研讨会,2002 年。
[4]
Interval-Based Memory Reclamation,Haosen Wen 等人,第 23 届 ACM SIGPLAN 并行编程 (PPoPP) 原则与实践研讨会论文集,2018 年。
是的,问题可能在 D19。
正如 指出的那样 不是正常的 free().
这在介绍 §1 中有说明
A node is freed only when no pointers in the data structure or temporary variables point to it.
我正在研究 Michael & Scott 的无锁队列算法并尝试用 C++ 实现它。
但是我在我的代码中产生了一个竞争,我认为算法中可能存在一个竞争。
我在这里阅读了论文: 简单、快速、实用的非阻塞和阻塞 并发队列算法 而原来的Dequeue伪代码如下:
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
在我看来,比赛是这样的:
- 线程 1 前进到 D3,然后停止。
- 线程2进阶到D3,读取与线程1相同的头
- 线程 2 幸运地一直推进到 D20,在 D19 它释放了 head.ptr
- 线程 1 继续并前进到 D4,尝试读取
head.ptr->next
,但由于head.ptr
已被线程 1 释放,因此发生崩溃。
而且我的 C++ 代码确实总是在线程 1 的 D4 处崩溃。
谁能指出我的错误并给出一些解释?
谢谢,非常有趣的主题!它看起来确实像一个错误,但该论文的一位作者断言他们的 free() 不是我们都使用的普通 free(),而是一些神奇的 free(),所以没有错误。太棒了。
希望没有人在没有彻底分析的情况下将其投入生产。
这实际上是自 MS 队列的作者之一 Maged Michael 引入 Hazard Pointers [1] 以来一直在探索的非阻塞内存回收问题。
危险指针允许线程保留块,以便其他线程在它完成之前不会真正回收它们。但是,这种机制会带来不小的性能开销。
还有许多基于 epoch 的回收变体,例如 RCU [2,3],以及最近的基于间隔的回收 (IBR) [4]。它们通过保留纪元来避免 use-after-free,并且比 Hazard Pointers 更快。据我所知,基于epoch的回收被广泛采用来处理这个问题。
您可以查看下面提到的这些论文以了解更多详细信息。 Interval-Based Memory Reclamation 的论文讨论了很多背景知识。
这是非阻塞数据结构中的普遍问题,我们一般不会将其视为数据结构本身的错误---毕竟它只出现在使用手动内存管理的语言中,如C/C++ 但不是像 Java 这样的(顺便说一句,Michael & Scott Queue 已经在 Java Concurrency 中被采用多年)。
参考:
[1] 危险提示:无锁对象的安全内存回收,Maged M. Michael,IEEE Transactions on Parallel and Distributed Systems,2004。
[2] 无锁同步的内存回收性能,Thomas E. Hart 等人,并行和分布式计算杂志,2007 年。
[3] 阅读副本更新,Paul E. McKenney 等人,渥太华 Linux 研讨会,2002 年。
[4] Interval-Based Memory Reclamation,Haosen Wen 等人,第 23 届 ACM SIGPLAN 并行编程 (PPoPP) 原则与实践研讨会论文集,2018 年。
是的,问题可能在 D19。
正如
这在介绍 §1 中有说明
A node is freed only when no pointers in the data structure or temporary variables point to it.