“MCS让B盯自己的节点,A负责通知B;CLH让B直接盯A的节点,A只宣布“我结束了”。前驱身份保存在等待者的私有pred中,因此释放者不必寻找或等待后继补一条next边。”
形式陈述
MCS锁把等待自旋锁的请求串成队列,每个等待者轮询自己的节点,由前驱直接通知它进入临界区。一个锁只有共享尾指针Tail;每个未完成获取使用独立节点q,含原子字段next和wait。
本页所有共享读写、exchange和CAS都处于顺序一致SC抽象模型。exchange是原子读改写,返回旧值并写入新值;CAS比较成功才更新。初始化Tail=null。节点在本次release完成前不能复用或释放;同一线程对多个锁或嵌套请求需要不同的在用节点。
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把锁,所需队列节点和尾指针为
参考资料
- John M. Mellor-Crummey and Michael L. Scott, “Algorithms for Scalable Synchronization on Shared-Memory Multiprocessors,” ACM TOCS 9(1), 1991, 21–65;作者维护的伪代码与后续算法说明,MCS部分