KAIST CS492: Design and Analysis of Concurrent Programs
#基本概念
#Progress
A program is making progress if, when the program threads are run for a sufficiently long time, at least one of the threads makes progress (for some sensible definition of progress).
#Make Progress
在多线程编程中,Make Progress 是指线程在全局时间上取得有用的进展。CAS 操作不算进展。
使用锁的问题是可能存在一段时间,程序所有线程都没有 Make Progress。例如,当一个线程拿到锁,然后被调度器切换到其他线程,此时,整个程序就没有任何 Progress。
#Lock-free
| An algorithm is lock-free if, when the program threads are run for a sufficiently long time, at least one of the threads makes progress (for some sensible definition of progress).
Lock-free 表示程序在任意时间内,至少有一个线程能够 Make Progress。
#Wait-free
| An algorithm is wait-free if every operation has a bound on the number of steps the algorithm will take before the operation completes.
Wait-free 表示程序在任意时间内,所有线程都能够 Make Progress。
关系: Wait-free ⊆ lock-free ⊆ obstruction-free ⊆ “nonblocking”
#Spinlocks
#Naive Spinlock
1 | /// A spin lock. |
#Ticket Lock
Ticket Lock 是一种自旋锁,它使用一个全局的计数器来实现锁。每个线程在进入临界区之前,需要先获取一个自增的 ticket,然后 spin 等待另一个计数器达到自己的 ticket 值。
加锁操作:
- 线程获取一个自增的 ticket 值。
- 线程等待另一个计数器达到自己的 ticket 值。
- 线程进入临界区。
解锁操作:
- 线程将另一个计数器加 1。
- 线程退出临界区。
1 | struct TicketLock { |
#MCS Lock
Ticket Lock 有一个问题,就是多个 CPU 同时争抢一把锁时,会导致严重的总线竞争,导致性能下降。
MCS Lock (Mellor-Crummey and Scott locks)[1] 通过使用一个链表来实现锁,每个线程在进入临界区之前,需要先获取一个自增的 ticket,然后 spin 等待另一个线程的锁。
数据结构:
- MCS 锁实例 : 一条单向链表的指针,初始化为指向一个未拥有的节点。
- 锁节点 : 每次加锁时创建一个节点,包含 next 指针和 locked 状态。
- next 指针指向下一个正在等待锁的节点。
- locked: volitile bool 状态表示当前线程是否正在持有锁。
加锁操作:
- 当前线程创建锁节点,next 设为 nullptr。
- 通过 lock.getAndSet()将自己加入队列尾部,并获取前驱节点 pred。
- 如果 pred 为空,则说明没有线程在排队,直接结束。
- 将当前锁节点的 locked 设为 true。
- 将 pred->next 指针指向当前节点。
- 自旋等待 locked 变成 false。
解锁操作:
- 获取当前线程的锁节点 I。
- 如果 I->next 为空,则说明没有线程在排队。
- 通过 lock.cas(I, nullptr)将队列清空
- 如果成功,则直接结束;否则,说明有后继节点过来了
- Spin 等待后继线程把 I->next 设置好
- 将 I->next->locked 设为 false。
1 | type qnode = record |
#CLH Lock
CLH Lock (Craig, Landin, and Hagersten locks)[2] 是一种基于 单向链表实现的可扩展、高性能、公平的自旋锁,它使用一个链表来实现锁。加锁过程相当于将当前线程加入到队列尾部,并等待前驱节点释放锁。解锁过程相当于将当前节点从队列中移除。
数据结构:
- CLH 锁实例 : 一条单向链表的指针,初始化为指向一个未拥有的节点。
- 锁节点 : 每次加锁时创建一个节点,包含 prev 指针和 succ_must_wait 状态。
- prev 指针指向前一个线程的锁节点。
- succ_must_wait: volitile bool 状态表示当前线程是否正在持有锁。
加锁操作:
- 当前线程创建锁节点,succ_must_wait 设为 true。
- 通过 lock.getAndSet()将自己加入队列尾部,并获取前驱节点。
- 在前驱节点的 succ_must_wait 上自旋,直到其变为 false。
解锁操作:
- 获取前驱节点。
- 将当前锁节点的 succ_must_wait 设为 false。
- 返回前一个锁节点,用于下一次加锁使用 (前一个锁节点一定不再被使用,这里复用前一个锁节点以避免内存分配和释放)。
1 | type qnode = record |
CLH 锁的优缺点:
- 空间复杂度低,N 个线程,L 个锁,每个线程获取一个锁,则存储空间为
O(N+L)
- NUMA 架构的 CPU 上,性能差。每个 CPU 都有自己的内存,如果前驱节点不在同一个 CPU 内存上,由于内存位置比较远,在自旋判断前驱节点的 locked 属性时,性能很差。不过在 SMP 架构的 CPU 上,不存在这个问题,性能依旧很高。
Java 的 AbstractQueuedSynchronizer(简称为 AQS)就是基于 CLH Lock 思想实现的。
#K42 Lock
K42 Lock 是一种 MCS Lock 的变种,由 IBM K42 Group 提出,避免了加锁时需要多传一个参数的问题。
1 | // Locks and queue nodes use the same data structure: |
#HCLH Lock
A Hierarchical CLH Queue Lock
#MCS Parking Lock
#总结
- Non-scalable locks are dangerous.
- 各种 Scalable Spinlock 的总结伪代码: https://www.cs.rochester.edu/research/synchronization/pseudocode/ss.html
#Lock-based Concurrency
#Sequence Lock
#Lock-free Data Structures
#Treiber Stack
Treiber Stack 是一种无锁栈实现,它通过使用一个链表来实现栈。
Push 操作:
- 创建一个新的节点 n,并将其数据初始化。
- top = load(top)
- n->next = top
- CAS(top, top, n)
- 如果失败,则回到第 2 步重试。
Pop 操作:
- 如果栈为空则返回 nullptr。
- top = load(top)
- next = top->next
- CAS(top, top, next)
- 如果 CAS 成功,则返回 top,否则回到第 2 步重试。
1 | impl<T> Stack<T> { |
#Michael-Scott’s Queue
Michael-Scott’s queue 是一种无锁队列实现,它通过使用一个链表来实现队列。
Enqueue 操作:
- 创建一个新的节点 n,并将其数据初始化。
- tail = load(tail)
- n->next = tail
- CAS(tail, tail, n)
- 如果失败,则回到第 2 步重试。
Dequeue 操作:和 Treiber Stack 的 Pop 操作一样
#Harris Sorted List
Mellor-Crummey, M. and Scott, M.L. 1981. Algorithms for Scalable Synchronization on Shared-Memory Multiprocessors. ACM TOCS, February 1991. ↩︎
Discovered independently by Travis Craig at the University of Washington (UW TR 93-02-02, February 1993), and by Anders Landin and Eric Hagersten of the Swedish Institute of Computer Science (IPPS, 1994). ↩︎