Skip to content

算法Algorithm

Harris 无锁有序链表

Harris linked list · Harris non-blocking list

用不可逆next标记分离逻辑删除与物理摘链,让插入、删除和帮助搜索共享可验证的CAS条件。

形式陈述 ​

Harris无锁有序链表用“先逻辑删除、再物理摘链”实现并发有序集合。每个节点保存不可变key,以及可由单字CAS原子比较/更新的 (next,mark)。mark=true表示本节点已逻辑删除,不是后继已删除。

设有不删除的−∞、+∞哨兵head、tail,键带有全序、集合无重复键。本页所有共享访问为SC,标记指针可在一个抽象原子字中表示;基础模型不释放或复用节点。实际指针位编码与回收是额外实现责任。

以下给出逐个帮助摘除标记节点的教学版本。findWindow(k)返回一对在本次搜索期间某时刻满足 pred.key<k≤curr.key、相邻且未标记的节点。

text
findWindow(k):
restart:
  pred = head
  curr = pointer(pred.next)
  repeat:
    (succ, marked) = curr.next
    if marked:
      if not CAS(pred.next, (curr,false), (succ,false)):
        goto restart
      curr = succ
    else:
      if curr.key >= k: return (pred,curr)
      pred = curr
      curr = succ

tail.key=+∞、tail.next的mark固定false,循环不会越过tail。修改操作为

text
add(k):
  repeat:
    (pred,curr) = findWindow(k)
    if curr.key == k: return false
    q = freshNode(key=k, next=(curr,false))
    if CAS(pred.next, (curr,false), (q,false)): return true
    discard unpublished q

remove(k):
  repeat:
    (pred,curr) = findWindow(k)
    if curr.key != k: return false
    (succ,marked) = curr.next
    if marked: continue
    if CAS(curr.next, (succ,false), (succ,true)):
      CAS(pred.next, (curr,false), (succ,false))
      return true

未发布的q可由创建者安全处理;已发布节点即使标记或摘链也不能直接释放。

直觉

直接从前驱跳过一个节点,可能切断别人刚接到该节点后面的新分支。mark先给该节点的next上一个不可逆的逻辑封条:它已经不在集合中,也不再允许插入者把新节点接在它后面。

物理摘链于是成为清理工作,任何搜索者都可帮助完成。移除者若标记成功后暂停,不会让别的线程永久等它更新前驱。

例子与边界

没有标记时怎样丢掉已成功插入的c ​

设a.key=1、b.key=3、d.key=7,链为a→b→d。错误删除器先读出b.next=d,打算CAS(a.next,b,d)。与此同时插入器要加入键5的c:

  1. 删除器保存旧后继d后暂停
  2. 插入器成功CAS(b.next,d,c),其中c.next=d,返回成功
  3. 删除器用旧快照成功CAS(a.next,b,d),返回成功

现在从a看见d,c和b一起被绕过。集合remove(3)却丢掉已经add(5)成功的元素,无法解释为合法的集合顺序历史。

标记与插入争夺同一next ​

正确删除器先在b.next上作标记CAS。

  • 若插入c先成功,b.next已经变为 (c,false),删除器原来期望 (d,false) 的CAS失败。它重读后继c,再把 (c,false) 改为 (c,true);随后摘b时连接a→c,保留插入结果
  • 若删除标记先成功,b.next为 (d,true),插入器要求 (d,false) 的CAS失败。重新搜索跳过b,随后可以把c接到a与d之间

mark与pointer必须属于同一次CAS比较。先用普通写单独置mark、同时允许别人按裸pointer CAS插入,并不能建立上述排他次序。

一次帮助搜索 ​

假设b已标记,物理链还是a→b→d。搜索键5时找到pred=a、curr=b,读取b.next=(d,true)。它尝试CAS(a.next,(b,false),(d,false));成功后从d继续,得到窗口(a,d),随后add(5)可把c接进去。

若a自身已经被别人逻辑删除,则a.next的mark为true,期待 (b,false) 的清理CAS会失败,搜索从head重来。因此不能在一个已删除前驱上“清理成功”后继续把新元素挂到失去共享可达性的链段。

线性化点分为更新与搜索见证 ​

成功add在线接q的CAS处生效,成功remove在curr.next的mark由false变true处生效。物理摘链不会再删除一次同一个集合元素。

contains(k)可调用findWindow后检查curr.key是否为k。contains结果以及add的重复键失败、remove的缺失键失败,均可在线性化于搜索期间满足窗口条件的某个时刻;不必硬指定最后一次key比较为那个时刻,因为窗口返回后仍可被别人修改。

例如搜索曾观察到未标记的键3,随后它被删除,但contains在返回前仍回答true。只要该节点确实在调用区间内某刻属于集合,就符合线性化。成功更新则靠CAS重新验证实际边或标记状态,不能仅依赖此前找到过一个窗口。

推论与应用

正确性使用三个关键不变量:可达未标记键严格有序;mark只从false变true,从不撤销;插入或清理CAS必须比较前驱next的未标记状态。标记以后,节点在抽象集合中消失;物理链可能暂时保留它,但搜索会跳过并尝试清理。

搜索沿键递增推进;若因CAS失败重新开始,相关链边已被其他线程改变。暂停线程不独占清理责任,其他搜索可继续移除标记节点。在无地址复用等基础假设下,这支持无锁进展,但并不保证指定请求在固定步数内完成。

若搜索路径含n个物理节点且无竞争,一次查找/更新为 O(n),成功插入只需一个关键CAS,删除包含一个标记CAS和至多一次直接清理尝试。竞争导致重启时,单次操作成本可无界。长链中已标记但未摘除的节点也算扫描成本,不能只按当前集合大小计。

加入Hazard Pointer时,前驱、当前节点及后继的保护和验证必须与具体搜索版本匹配;只给原论文每个裸指针读取前随手插一条hazard写,不能自动证明安全。尤其从一个已摘除但仍受保护的节点沿旧next前进,未必能直接证明后继仍可保护。本页基础伪代码将回收分离,应用时应采用已经证明的兼容遍历协议。

参考资料
关系图谱14 个相邻概念 · 2 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
类型化关系