Skip to content

2026 大纲 二(三)3 锁

把进入区和退出区包起来,叫它锁

上一节的各种写法有个共同的不方便:每次要保护一段临界区, 都得把进入区那几行代码原样抄一遍。抄错一个字(比如 Peterson 里 turn 赋反了) 就前功尽弃。

自然的做法是封装:把进入区包成 acquire(),把退出区包成 release()。 这就是

⚠️ 所以锁不是一种新机制,它只是对底层互斥手段的一层封装—— 底下可以是 TSL、可以是 Swap、也可以是信号量。 上一节那四条准则可以原样拿来评价一把锁。

封装之后有一个新的设计问题浮出来:拿不到锁的时候干什么?

两种选择,也就是两类锁:一直循环重试自旋锁,忙等)、 让出处理机去睡互斥锁,阻塞)。 这一条就是两者唯一的分界,剩下的差别全是它的推论。

判据是比临界区长度与两次上下文切换的开销:临界区很短时, 自旋几圈就等到了,比睡一觉再被叫醒便宜;临界区长则反过来。

⚠️ 还有一条边界要立住:自旋锁在单处理机上毫无意义。 因为持锁者此刻并没有在运行(CPU 被自旋的这个进程占着), 它等的是一个当下不可能被释放的锁——除非被时钟中断切走,否则死等。

一、锁封装了什么

acquire(lock);     // 获取锁 —— 就是「进入区」
临界区;
release(lock);     // 释放锁 —— 就是「退出区」

二、自旋锁

c
// 基于 TSL 实现
void acquire(bool *lock) {
    while (TestAndSet(lock));   // 一直循环直到读到旧值 false
}
void release(bool *lock) {
    *lock = false;
}

自旋等待的那个条件,只能由被自旋者挤下去的那个进程来改变——这就是单处理机上自旋无意义的全部理由。多处理机上持锁者能在另一个核上并行跑完临界区,转几圈就可能等到锁被释放,"转"才比"切出去再切回来"划算。

多核上的改法(思路比代码重要)——先只读地自旋,看起来空了再动手抢:

c
void acquire(bool *lock) {
    while (true) {
        while (*lock) ;              // ① 只读自旋:命中本核缓存,不产生写
        if (!TestAndSet(lock)) return;  // ② 看着像空的,才真正去抢
    }
}

① 只读,缓存行可被多个核共享持有,不会互相踢;只有在锁看起来空了时才走 ②,产生一次写。这样把"每圈一次写"降成"锁状态变化时才写一次"。

三、互斥锁

void acquire(lock) {
    if (lock 已被占用) {
        将自己加入该锁的等待队列;
        block();               // 让出处理机,进入阻塞态
    }
    lock = 已占用;
}

void release(lock) {
    lock = 可用;
    if (等待队列非空)
        wakeup(队首进程);      // 把一个等待者移入就绪队列
}

它满足全部四条准则,代价是每次等待要付两次上下文切换——而上下文切换不只是保存/恢复寄存器,它还会让高速缓存和 TLB 里属于原进程的内容失效,切回来时要重新预热。

四、两种锁的对照

自旋成本临界区长度vs阻塞成本2×一次上下文切换
判断维度选自旋锁选互斥锁
处理机数多处理机(持锁者能在别的核上推进)单处理机、多处理机都可以
临界区长度很短(短于两次上下文切换)较长或长度不可预知
调用位置中断处理程序等不能睡眠的上下文进程上下文
让权等待不满足满足
主要开销空转的处理机时间 + 跨核缓存失效两次上下文切换 + 缓存/TLB 预热

五、锁的粒度

粗粒度(一把大锁)细粒度(多把小锁)
做法整个数据结构一把锁按分区/桶/行各一把锁
并发度低——互不相干的操作也被串行化高——不同分区可并行
单次开销小,加解锁各一次大,可能要连续拿多把锁
出错风险:多把锁就可能形成循环等待
什么时候合适竞争本来就少,或临界区极短竞争激烈且访问天然可分区

推导链:锁的作用是把并发变成串行 ⇒ 被锁圈住的范围越大,被强制串行的工作越多 ⇒ 想提高并发就得把范围切小 ⇒ 切小意味着一次操作可能要同时拿多把锁 ⇒ 多把锁 + 不同的申请顺序 = 循环等待同步互斥基本概念里"剩余区必须摘出去"本质上也是粒度问题——把无关代码留在临界区里,等于无谓地把粒度调粗。

考点速记

  1. 锁是对底层互斥机制的封装acquire / release 正好落在进入区退出区四条准则可直接用来评价它。
  2. ⚠️自旋锁与互斥锁的分界只有一条:拿不到锁时让不让出处理机。 自旋锁忙等,互斥锁阻塞。
  3. ⚠️自旋锁在单处理机上毫无意义——持锁者此刻并没在运行,它等的是一个当下不可能被释放的锁
  4. 自旋锁在多核上也不免费:每圈都写锁变量会引发跨核缓存失效,改法是先只读自旋、看到可能空闲了再去抢
  5. 选型判据是比临界区长度与两次上下文切换的开销:临界区短用自旋锁,长用互斥锁。
  6. ⚠️互斥锁就是初值为 1 的信号量,但锁有"所有权"(谁加的锁谁解),因而表达不了同步——同步需要"甲做完某事、乙才能继续",而解锁的人和加锁的人必须是同一个。
  7. 能力链是 锁 < 信号量 < 管程:锁只能互斥,信号量能互斥也能同步,管程再加上封装与条件变量。
  8. 粒度调细必须配"按固定顺序申请锁"的规矩,否则会死锁(见死锁的概念与预防的资源有序分配法)。

这一节在真题里被考过的形式

锁本身至今不单独成题。 本页下方练习区渲染的是整个 sync-mutex-concepts 标签下的 18 道题,范围比本节宽,属正常——那些题分别在 同步与互斥基本概念同步互斥实现方法信号量 与三个经典同步问题里讲。

它不单独成题,但两条结论是选项里的常见素材

第一,速记第六条(互斥锁 = 初值为 1 的信号量,但锁表达不了同步)。 凡出现"能否用锁实现前驱关系""互斥锁和信号量有什么区别"这类问法, 答案都由它给出——分界是有没有所有权

第二,速记第三条(自旋锁在单处理机上无意义)。 它是"为什么多核编程才大量用自旋锁" 这类判断的依据,也与同步互斥实现方法速记第六条 (关中断在多核上失效)构成一对:单核与多核,各有各的方法失效。

复习优先级读懂即可,重点是第二、六条。 "拿不到锁让不让出处理机"这条分界 与"互斥锁表达不了同步"这条结论要记住,其余理解一遍就够。 第四条那个缓存失效属于体系结构的地界,不必细究。

易错:认为锁是一种独立于信号量的新机制。它是对底层互斥手段的封装,互斥锁就是初值为 1 的信号量。

易错:认为自旋锁在单处理机上也有用。持锁者此刻没在运行,等的是一个不可能被释放的锁。

易错:认为可以用互斥锁实现进程同步。锁有所有权——加锁和解锁必须是同一个进程,表达不了"甲做完乙才能走"。

易错:认为锁的粒度越细越好。细粒度必须配"按固定顺序申请"的规矩,否则会死锁。

教材出处
  • "把标志看做一个锁":汤小丹《计算机操作系统》2.4.2 节,印刷 p51——"锁开"进入、"锁关"等待,初始时锁是打开的;测试和关锁操作必须是连续的。
  • 自旋锁的引入与总线竞争:同书 10.4.2 节,印刷 p321——多处理机上读—修改—写原语包含若干条指令、需多次总线操作,总线由多个处理机竞争获取;自旋锁与信号量的主要差别在于自旋锁可避免调用进程阻塞,而进程切换"需要花费一定开销,并且会使高速缓存失效";用自旋锁保护的临界区一般都应比较短。
  • 单 CPU 下自旋锁退化为空操作:同书印刷 p322——"自旋锁只有在内核可抢占或 SMP 的情况下才真正需要,在单 CPU 且不可抢占的内核下……不需要自旋锁,此时自旋锁的所有操作都是空操作。"同页还给出了选型判据(临界区大用信号量、中断上下文或临界区极小用自旋锁)与读写自旋锁的定义。
  • 互斥锁是初值为 1 的信号量:同书 2.4.3 节记录型信号量,印刷 p54——"如果 S->value 的初值为 1,表示只允许一个进程访问临界资源,此时的信号量转化为互斥信号量。"

相关知识

同步互斥实现方法同步与互斥的基本概念信号量与 PV 操作管程与条件变量读者-写者问题死锁的概念

真题练习