本文是AQS与CLH相关论文学习系列第三篇。 系列其他文章链接如下
本文在如上两篇文章的基础上, 进一步学习 CLH 锁设计者 Craig, Landin and Hagersten 的论文。
参考文章
-
Building FIFO and priority-queueing spin locks from atomic swap - CLH 队列之 Craig 论文
-
Queue Locks on Cache Coherent Multiprocessors - CLH 队列之 Landin, Hagersten 论文缩减版
CLH 锁的基础算法介绍
为了更好地理解 CLH 锁, 笔者对【参考文章1】【参考文章2】【参考文章3】 都进行了阅读, 发现与 Landin and Hagersten 的论文相比,还是 Craig 的论文更为清晰易懂。 所以主要借助 Criag 文中的逻辑来学习 CLH 锁。
如之前所介绍的, MCS 锁是先于 CLH 锁提出的基于链表结构的排队锁, Criag 和 Landin and Hagersten 的论文中都有 MCS 锁论文的引用, CLH 锁其实就是在 MCS 锁基础上做了一些改进。
Craig 的论文很注重概念的清晰化定义, 在介绍算法前, 先单独明确了几个论文中频繁使用的概念
- process: 论文中所描述的 process , 表达的是一个可调度在 CPU 上运行的实体, 对于某些系统来说, 这可能是一个线程。
- 三种数据结构:
- Lock
- 对于每一个锁来说, 都会分配构建一个 Lock 类型的数据记录。 对于一般的锁的数据记录来说, 其内部通常会包含一个标识, 表达锁的是否已经被抢占的状态, 但是在链表式排队所的结构中, 这个锁记录就包含一个指针(这一点和 MCS 锁完全一样)
- Process
- 每个进程都会有一个 Process 类型的数据记录。其实就是进程级别的私有变量数据。
- Request
- 每当进程想请求一个锁时, 就需要将一个 Request 类型的数据记录加入对应锁的请求队列中,注意, 这个 Request 类型的数据记录甚至直接可以是 Process 记录本身, 因为锁队列中的结点其实就是一个进程获取锁的代表, 只要有一个可以与进程一对一关联的数据记录即可。 Request 概念上和 CMS 锁中提到的 qnode 一样, 都是锁队列中的结点类型数据。
- Request 记录中包含一个状态变量 state , 取值为 PENDING 或 GRANTED
- Lock
下图是论文中对这三种数据类型定义的伪代码。 有趣的是这一伪代码风格和 MCS 论文中的伪代码风格相近但不完全一致, 但 Craig 在论文中提到了 Pascal, 笔者查询后发现, 指针的表达方式确实是 Pascal 的语法,看伪代码别扭的同学可以先搜一下 pascal 的指针等语法, 应该会有帮助。
小提示
- 伪代码方法声明
method( varName : type)
, 等价于于 java 语法的method(Class var)
^someType
代表someType
类型的指针, 按照笔者搜索结果, 好像是 VC++ 中的语法, 代表 managed pointer, 所指区域可以被垃圾回收器自动回收, 无需手动释放。something^
代表指针something
所指向的变量
数据结构图形化表达如下
下面是锁和进程的初始化伪代码
值得注意的是, 上面的 init_Lock 部分, 将指针 L.tail 指向了一个新分配的 Request 结点, 并且将该 Request 记录的 state 设置为了 GRANTED。 这里与 MCS 锁不同, 因为 MCS 锁的Lock 的指针起始状态是指向 nil (null) 的。
下图展示了 1个锁与 3 个进程的初始化后的状态
加锁操作的伪代码
加锁操作其实就是将本进程对应的 Request 结点的state 状态置为 pending, 然后本进程的 watch 指针 L 所指的队尾结点, 同时将 L 指针指向新加入的队尾结点, 整个操作利用了 fetch&store 这一原子指令确保并发入队的安全性。整个过程其实就是让当前进程监控 Lock 队列中原本的队尾 Request 结点, 根据队尾结点的 state 状态, 判断自己是否能获得锁, 将自己构建的 Request 入队为新的队尾, 供后继者监控。
下图为进程 P 1 P_1 P1 执行了 request_lock 之后的状态, 此时由于 P 1 P_1 P1 是第一个请求锁的进程, 它自旋监控的结点就是 Lock 初始化时创建的队尾 L 指向的 R 0 R_0 R0 结点, 其状态是 Granted, 这样就代表 P1 应持有了锁, P 1 P_1 P1 自己入队的结点 R 1 R_1 R1 状态置为 PENDING, 供后继者入队监控
下图展示了 P 1 P_1 P1 在未释放锁的情况下, P 2 P_2 P2, P 3 P_3 P3 也请求获取锁, 入队等待的状态。 看一个每一进程入队都是去 watch 监控 L 指向的队尾 Request, 然后将 L 指向自己所持有的 Request , 状态置为 PENDING, 供后续入队者监控 watch 。
- 提示: 下图的绘制队尾在左, 队头在右 ,和我们平时的习惯队头在左, 队尾在右的习惯不太一样, 可能第一眼看起来会有点别扭
至此加锁入队的操作就基本清楚了, 然后看下释放锁的操作, 伪代码如下
-
锁持有进程在释放锁的时候, 只需要将自己持有的 myReq 记录中的 state 状态置为 GRANTED, 就相当于通知后继者可以通过自旋, 把锁的所有权让渡给了后继者
-
另外这里还有一个非常有技巧性的操作是, 把自己原来 watch 监控的前驱结点 Request 变成自己持有的 myReq, 相当于废物利用, 回收了无用的 Request, 自己在下一次需要加锁时, 不需要重新再构建分配新的 Request
下图展示了 P 1 P_1 P1 进程把锁的持有权传递给了 P 2 P_2 P2 后的状态, 此时 P 2 P_2 P2 为持有锁的进程
下图则为 P2, P3 依次都获得并释放锁后, 无人持有锁的状态。注意, 整个数据结构的状态又恢复到了三个进程和锁初始化时等价的状态, 非常精巧
至此, 整个 CLH 队列的基本逻辑就已经清晰了, 由于上述部分都是 Craig 论文中的内容, 笔者也将 Landin and Hagersten 论文自己命名的 LH 锁的伪代码和部分图片摘录出来进行比较, 以证明两篇论文里说到的数据结构就是同一种, 这也是后人为什么将该数据结称为 CLH 锁的原因
下图与上图说的状态完全等价,P1持有锁, P2,P3 在等待, 只是队头方向画在了右边
CLH 锁与MCS 锁的对比分析
回过头来思考 CLH 锁与 MCS 锁的关系, 其实可以发现, CLH 锁相比 MCS 锁, 最明显的改进就是其释放锁的操作中, 没有自旋。 这很大程度上降低了锁的所有权转移过程的开销。
CLH 锁作为一个与 MCS 锁结构高度相似的锁, 之所以可以避免锁释放操作的自旋, 主要得益于如下设计思想的微调
同样都是每个进程对应于一个队列结点
- MCS 锁判断一个进程是否已经获得锁的依据是进程本身持有的队列结点其中的某个值的状态
- CLH 锁判断一个进程是否已经获得锁的依据是进程本身持有队列结点的前驱结点中某个值的状态
基于上述差别
- MCS 锁的持有进程在让渡锁的所有权时, 由于需要关心自己的后继结点是否存在以及是否会被突然添加, 所以多了一些负担
- MCS 锁在持有进程在让渡锁的所有权时,由于已经知道后继结点肯定只能监控自己在入队时就设置好的结点, 所以无需关心是否存在后继结点, 只需要修改自己预留给后继结点监控的队列结点状态即可。
从上述差别, 笔者其实想大胆的意淫一下, 虽然 Craig 和 Landin and Hagersten 都引用了 MCS 的论文, 但是都没有去详细分析自己设计的 CLH 锁与 MCS 锁之间的相似性和差异, 这或许反映了学术工作中为体现创新, 在行文过程中使用的一点小心思
CLH 锁如何添加优先级等特性
前面介绍的CLH锁算法实现了基本的先入先出自旋锁排队, 但是 Craig 大佬没有止步于此, 论文的后半部分进一步分析了如何为 CLH 锁添加如下高级特性:
- 支持嵌套: 允许一个进程同时持有多把锁
- 支持超时: 一个进程一段时间未获取到锁可以结束锁获取请求
- 条件式获取锁:允许进程在锁可用的情况下才获取到锁, 否则就立刻返回
- 支持抢占式获取锁: 在保证锁不会被非运行态的进程获取基础上, 允许调度器抢占掉某个运行态进程的锁获取机会。
Craig 大佬指出, 在下图基础版本的 CLH 锁的队列结构其实并不是一个常规意义的链表, 因为对于任意一个进程, 都无法顺着 L 指针或者完成整个队列的遍历。 链表中的结点其实没有前驱和后继指针相连, 前驱后继关系完全是通过进程的排队实现的。
在此基础上想要实现支持基于优先级队列顺序获取锁其实也不难, 只需要添加一些指针, 允许锁的持有进程可以遍历链表, 在释放锁的时候将锁传递给优先级最高的进程。
具体而言, 可以添加如下指针
- 为锁队列添加一个头指针 head
- 为队列结点添加一个指针 watcher, 指向正在监控watch 它的进程, 成为 process 中 watch 的反向指针
- 为队列结点添加一个指针 myproc, 指向拥有它的进程,成为process 中 myreq 的反向指针
- 注意下图中的 Process 结构中的
pri