Skip to content

算法Algorithm

MCS 排队自旋锁

MCS lock · Mellor-Crummey–Scott queue lock

通过每请求节点和前驱直接交接实现FIFO自旋,正确处理Tail更新先于next发布的窗口。

形式陈述 ​

MCS锁把等待自旋锁的请求串成队列,每个等待者轮询自己的节点,由前驱直接通知它进入临界区。一个锁只有共享尾指针Tail;每个未完成获取使用独立节点q,含原子字段next和wait。

本页所有共享读写、exchange和CAS都处于顺序一致SC抽象模型。exchange是原子读改写,返回旧值并写入新值;CAS比较成功才更新。初始化Tail=null。节点在本次release完成前不能复用或释放;同一线程对多个锁或嵌套请求需要不同的在用节点。

text
acquire(q):
  q.next = null
  q.wait = true
  p = exchange(Tail, q)
  if p != null:
    p.next = q
    while q.wait: skip
  return

release(q):
  s = q.next
  if s == null:
    if CAS(Tail, q, null): return
    repeat s = q.next until s != null
  s.wait = false

q.wait表示本请求是否仍在等待前驱交接;无前驱的请求直接进入,不需要读取自己的初始true。实现应保证节点字段的原子可见性以及临界区数据的同步;上述SC伪代码不是可以直接使用普通C++字段的弱内存实现。

直觉

集中式自旋让所有线程围着一个热点反复查看。MCS则给每个排队者一张私人号码牌:只盯自己的wait,轮到自己时,前一个人把它改成false。

与CLH锁轮询前驱字段不同,这里轮询者持续使用自己的节点;这一差别决定了DSM布局和释放补链的不同责任。

Tail上的exchange确定排队顺序;next把这个顺序补成前向链。两步不是同时发生的,所以“已经排在队尾”和“前驱已经看见next”之间会有窗口。release里看似多余的CAS和等待,正是为了补上这个窗口。

例子与边界

三个线程怎样排队 ​

A首先交换Tail,取得null,因此进入临界区。B交换得到A节点,设置A.next=B,然后只读B.wait。C交换得到B,设置B.next=C,然后只读C.wait。

此时逻辑顺序为A、B、C。A释放时将B.wait改为false,B进入;C仍在等自己节点上的true。B释放时再将C.wait改为false。一次释放只需直接唤醒后继,不必广播给所有等待线程竞争下一次CAS。

这里的FIFO按exchange发生顺序定义,不按线程在源代码中开始调用的墙上时间排序。B可能更早调用acquire,却被调度器暂停在exchange之前,让C先完成排队。

Tail已变,next还没出现 ​

设A持锁,A.next=null。

步骤 动作 Tail A.next
1 B执行exchange,得到前驱A,随后暂停 B null
2 A开始release,读到next=null B null
3 A尝试CAS(Tail,A,null) B,CAS失败 null
4 A等待后继把链指针补齐 B null
5 B恢复,写A.next=B B B
6 A写B.wait=false,完成交接 B B

第3步的失败告诉A:已有请求排在后面,只是链还没连好。如果A直接返回并复用自己的节点,B随后写A.next可能破坏另一轮队列,B.wait也可能永远没人清除。

相反,若CAS成功,则在那个原子时刻Tail仍是A,确实没有已排队后继。后来到达的新请求会交换到null,独立成为新一轮持锁者。无需在“队列可能随时变动”这个事实下永远等待。

互斥与交接不变量 ​

按exchange顺序,每个非首请求恰有一个前驱。它只能在自己的wait被该前驱release清除后进入;其他等待者不会写这个标志。前驱先结束临界区,再交接标志,故两个相邻请求不会同时处于临界区。

首请求凭exchange返回null进入。只有最后持锁者释放时才能把Tail改回null,因此不存在旧队列仍有合法等待者、另一请求却被当作独立首请求进入的情形。释放时的补链等待正用于维持这一点。

推论与应用

若用线性化描述锁的抽象状态,无竞争获取可在返回null的exchange处生效;有竞争获取与前驱的释放交接相邻排列。不能把一个排队很早、尚未获准进入的请求在其exchange处就视为已经持锁。

请求排队之后不会被后来的exchange插队,但无饥饿还依赖调度与公平条件:每个前驱最终继续运行、退出临界区并完成补链/交接。若持锁者或队头线程永久暂停,后继仍会被挡住。MCS是排队锁,不是lock-free算法。

不计等待循环,每次获取/释放只做常数次共享更新。若节点等待字段独占适当缓存行,在缓存一致CC模型中,等待读可命中本地缓存;在DSM模型中把自己的q放在本地内存,轮询也可为本地访问。标准RMR模型下每次请求只产生常数次远程内存访问,但本地自旋步数和墙钟等待时间可能无界。伪共享、节点放错位置或线程迁移都可能破坏理想的通信成本。

若有p个同时在用请求、n把锁,所需队列节点和尾指针为 O(p+n) 空间。节点的生存期是协议的一部分,不能因为它最初声明为线程局部变量,就在release尚未交接时让其存储失效。

参考资料
关系图谱11 个相邻概念 · 4 类关系

拖动节点调整位置。

显示关系

显示:依赖

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