我推荐阅读 introduced the term 的论文 - 主要作者 Herlihy 在 25 年前引入 wait-freedom 的概念时开创了整个行业。
无锁和无阻塞之间的核心区别在于,如果两个或多个线程正在运行,后者不保证进度(但如果只有一个在运行)。
论文的内容是你想要的,一个无阻塞但不是无锁的出列示例。
为了更简单,我将描述一个基于数组的堆栈,它以相同的方式运行,并且是无阻塞但不是无锁的。
想象一个在数组顶部实现的堆栈,这样堆栈的零个或多个元素连续存储在数组的开头,然后是所有剩余位置的“空”元素。堆栈的每个元素都存储为一个元组:(val, seq),其中val 是用户提供的值,seq 是一个序列号,它是算法的关键(同时也避免了 ABA 问题1 )。
要将元素压入堆栈,首先要找到堆栈中的最后一个元素(位置A[k-1]),它直接位于第一个null 元素之前(位置A[k])。您尝试使用 CAS 增加 A[k-1] 的序列号(元素不会更改),如果成功,您尝试同时替换位置 A[k] 处的空元素的值并增加其序列号。如果任一 CAS 失败,请重试。
pop 算法类似,但以相反的顺序处理元素(增加第一个 null 元素的序列号,然后尝试使最后一个元素为 null 并增加其序列号)。
这个结构的正确性在上面的论文中有更详细的描述,但基本上通过成功增加A[k-1]th 元素,您可以确保此时它仍然是列表中的最后一个元素,并且您同时通知任何通过导致初始 CAS 失败,您可以“获得下一个镜头”的竞速推进操作。如果第二个 CAS 成功,则您成功添加了一个元素。
请注意,并发的 push 和 pop 操作可能会导致彼此无限期地失败。 push 线程递增A[k-1],pop 递增A[k],然后每个线程都失败了,因为它们看到了另一个线程的增量。冲洗并重复。这是活锁的一个例子。
请注意,如果只有一个线程在运行,问题就会消失,这是无阻塞的关键观察。
1 ...它还避免了完整版本的出队中的“环绕”问题,但我认为堆栈不会发生这种情况。