想象你经营一家小书店。店里只有一个收银台,顾客买书得排队。这时你会遇到三种情况,正好对应并发编程里的三把“锁”。 第一种,排队的人不多,大家都站在收银台前盯着店员找钱——这就是自旋锁:等待的人不离开,在原地打转(自旋),店员刚喊“下一位”,顾客立刻接上。 第二种,书店里开了一块阅读区,很多顾客可以同时看书;但店员要整理书架时,必须请所有读者放下书离开。这就是读写锁:读者之间互不干扰,写者独占禁区。 第三种,书店更新书目。店员不需要喊“所有人放下书”,而是把新版书挂到公告栏上,老顾客继续读完手里的旧版,新顾客直接拿新版;店员等最后一个读旧版的顾客离开,再把旧书收走。这就是 RCU。 电脑里,无数线程就像这些顾客,争抢着同一份数据。选错锁,好比让顾客在收银台前干等一小时,或让所有看书的人反复被打断。这篇文章,就用这间小书店,带你摸清三种锁的脾气、毛病,以及什么时候该用哪一把。

岗位关联

这套知识是理解 Linux 内核、数据库引擎和高并发中间件的底层钥匙,掌握它,你才能在系统设计面试和性能调优中真正看穿并发瓶颈。

目录
  1. 可视化演示
  2. 自旋锁原理
  3. 自旋锁问题
  4. 读写锁设计
  5. 锁的公平性
  6. 内存屏障
  7. RCU核心思想
  8. RCU宽限期
  9. 三锁对比选型
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

通过自旋锁忙等待、读写锁并发控制和RCU宽限期回收三个模式,对比三种内核同步机制的适用场景与性能特征。

本节配有交互动效(进入视口后自动加载,约 3KB)。

自旋锁忙等待机制 多核竞争同一锁,未获锁的CPU持续忙等(自旋)不睡眠 适用于临界区极短场景,避免上下文切换开销 while(!try_lock()) { /* spin */ } // busy-wait, no sleep 自旋锁在等待时不释放CPU,临界区必须极短以免浪费CPU周期

自旋锁原理

收银台前只有两三位顾客时,你会怎么做?站着等。眼睛盯着店员找钱,手随时准备把书递上去,店员刚说完“下一位”,你立刻接棒。没有离开,没有绕一圈再回来——这就是“自旋”:人还在原地,注意力始终锁定目标。

操作系统里的线程竞争共享变量时,处境完全一样。线程 A 进入临界区前要先拿锁,其他线程只能等着。自旋锁的等待方式很特别:它们不睡觉、不让出 CPU,而是在一个循环里反复尝试获取锁。这种“原地打转”的等待,学名叫忙等待(busy-waiting)。

自旋锁能成立,靠的是硬件原子指令,如 test-and-set、xchg。它们在 CPU 层面把“读旧值—改新值—写回”三步合成一步,中间不会被打断。拿到锁的线程在临界区执行完立即释放,等锁线程几乎零延迟补位,省掉了上下文切换(context switch)的开销——一次切换动辄几十到几百纳秒,而自旋一轮往往只要几纳秒。

回到书店,收银台前排着两三个人时,站着等确实比“先走开、过会儿再来”快得多。可排队的人一旦多起来,问题就来了——下一节,我们看看自旋锁的困境。

tspin<tswitch  ⟹  适合使用自旋锁t_{\text{spin}} < t_{\text{switch}} \;\Longrightarrow\; \text{适合使用自旋锁}
自旋等待的总时间如果小于一次线程切换的耗时,忙等待策略就划算。
  1. 符号定义$t_{\text{spin}}$ 表示线程自旋等待并成功进入临界区的总耗时。
    tspint_{\text{spin}}
  2. 符号定义$t_{\text{switch}}$ 表示一次上下文切换(保存/恢复线程状态)的固定开销。
    tswitcht_{\text{switch}}
  3. 判据含义当 $t_{\text{spin}} < t_{\text{switch}}$ 时,与其让线程休眠切换,不如让它原地自旋,用更短的时间换取更低的延迟。

自旋锁问题

促销日,收银台前排起长龙。你还敢站在台前干等吗?每个人都原地转圈、跺脚,整个店堂挤成一团,谁也没法去干别的,后面的人甚至挤不进来。你突然意识到:等待本身也在消耗精力。

自旋锁最大的毛病正是空转烧 CPU。等待的线程不睡眠,占着处理器不断轮询,白白消耗算力。等锁线程越多,浪费越严重。它还不公平:后来的线程可能先抢到锁,先来的反而一直抢不到——理论上甚至存在饥饿风险。此外,自旋锁要求临界区极短:临界区一旦拖长,自旋等待的成本就会反超上下文切换,得不偿失。

单核 CPU 上问题更尖锐:拿着锁的线程被抢占后,等锁线程在同一颗核上空转,永远也等不到锁被释放,变成死等。因此自旋锁在单核上必须配合关闭抢占或中断使用,确保持锁线程能一路走完临界区。

小书店的教训是:自旋锁只适合“人少、结账快”的时刻。当家大了、需求复杂了,就得设计更精细的排队规则——读写锁登场。

Twaste=nwaiter×tspinT_{\text{waste}} = n_{\text{waiter}} \times t_{\text{spin}}
忙等线程数越多、自旋时间越长,浪费的 CPU 周期就越可观。
  1. 符号定义$n_{\text{waiter}}$ 表示当前排队自旋的等待线程数量。
    nwaitern_{\text{waiter}}
  2. 符号定义$t_{\text{spin}}$ 表示每个等锁线程单次自旋尝试的耗时。
    tspint_{\text{spin}}
  3. 乘积含义总浪费时间等于等待线程数与自旋耗时的乘积,说明等的人越多,浪费线性放大。

读写锁设计

书店扩张,你开辟了一块阅读区。看书的顾客之间互不打扰,几十个人能同时坐着翻书——“读”是共享的。但店员要往书架上补新书、改价签时,必须先请所有读者放下书离开,整理完成再开放——“写”是独占的。

读写锁(Reader-Writer Lock)正是这个逻辑。它把加锁分成两种:读锁和写锁。任意多个线程可以同时持有读锁;而写锁同时只能有一个线程持有,并且与其他任何读锁或写锁互斥。核心结构是一个“读者计数 + 写锁标志”的组合。

实现上,读者获取读锁时把读者计数加 1;如果自己是第一位读者,还要尝试获取底层的那把“大锁”。写者必须独占这把大锁。这意味着,当有写者在排队时,新来的读者会被挡在门外,以免读者源源不断涌入、写者永远轮不到。这种设计天生适合缓存、配置中心这类读多写少的场景。

阅读区虽然热闹,却埋下一个隐患:店员总来整理书架,读者会被反复清场;读者太多,整理书架的店员可能永远轮不到动手。这就是下一节要谈的公平性。

T读时段=max⁡(tr1, tr2, …, trn)T_{\text{读时段}} = \max\left(t_{r_1},\, t_{r_2},\, \ldots,\, t_{r_n}\right)
n 个读者并行进入读区,整体耗时等于最慢那位读者,而不是所有读者耗时的累加。
  1. 符号定义$t_{r_1}, t_{r_2}, \ldots, t_{r_n}$ 表示 n 个读者各自在临界区内的停留时间。
    tr1,tr2,…,trnt_{r_1}, t_{r_2}, \ldots, t_{r_n}
  2. 取最大值$\max$ 表示取序列中最大的一个。
    max⁡\max
  3. 物理含义由于读锁共享,所有读者同时执行,总耗时才等效于“最慢读者”,这正是读写锁提升并发度的数学来源。

锁的公平性

你发现阅读区的奇怪现象:店员想整理书架,读者却一个接一个进来,眼看永远轮不到店员;反过来,店员频繁清场,读者又抱怨连一页书都看不完。到底谁先来、谁后到?这就是并发世界的公平性(Fairness)问题。

计算机里,读写锁面临典型的写者饥饿:读者不断申请读锁并共享进入,写者却一直等不到写锁。解决思路是让先来者先服务。比如排队票号锁(Ticket Lock):每个线程领一张票,号码到了才能进;读写锁内部也可以维护等待队列,让写者排在读者前面,保证写者不被读者无限压制。

公平性不是开关,而是一组权衡。写者优先会略微牺牲读者的并发度;读者优先则可能让写者饿死;理想方案是“先来后到、按序放行”。Linux 内核的 rwsem 就采用写者优先策略,避免系统长期停留在只读状态。

排队规则定得再好,也挡不住一个底层问题:即使线程按顺序执行,我们看到的“先后”一定真实吗?CPU 乱序执行和缓存机制,会让代码顺序与真实发生顺序不一致。要解决这个问题,得请出内存屏障。

tw(k+1)=∑i=1ktcit_w^{(k+1)} = \sum_{i=1}^{k} t_{c_i}
第 k+1 个排队线程的等待时间,等于前 k 个线程临界区执行时间之和。
  1. 符号定义$t_w^{(k+1)}$ 表示第 k+1 个排队者从排队到进入临界区的等待时长。
    tw(k+1)t_w^{(k+1)}
  2. 累加符号$\sum_{i=1}^{k} t_{c_i}$ 表示把前 k 个线程各自占用的临界区时间加起来。
    ∑i=1ktci\sum_{i=1}^{k} t_{c_i}
  3. 含义前面任何线程的临界区变长,都会直接推高后面所有人的等待时间,这就是不公平感被放大的根源。

内存屏障

收银台前,店员照单据叫号,顾客却发现:明明先来的人先付款,系统里记录的却是后付款的人先完成。原因在于“执行顺序”和“记账顺序”被悄悄调换了。为了杜绝混乱,书店规定:所有收据必须盖章,按章号顺序入账。

CPU 为了提速,会乱序执行指令(Out-of-Order Execution),还会把写操作暂存在自己的 store buffer 里,导致其他核心看不到最新值。锁的实现必须配合内存屏障(Memory Barrier),它像一道盖章关卡,强制屏障前后的内存操作按序全局可见。

具体来说,sfence 保证屏障前的写操作先于屏障后的写操作被其他核看到;lfence 保证读操作不被重排;mfence 同时约束读写。Linux 内核用 smp_mb() 等宏在需要处插入屏障。自旋锁、读写锁内部都藏着屏障——这也是锁除互斥外最重要的职责:提供内存可见性。

有了屏障,原子操作才真正“像盖章一样”算数。但所有锁多少都要阻塞或等待,能不能让读端彻底无锁?RCU 给出了一个不一样的答案。

Wa≺sfenceWb  ⟹  Wb 在全局顺序中晚于 WaW_a \prec_{\text{sfence}} W_b \;\Longrightarrow\; W_b \text{ 在全局顺序中晚于 } W_a
屏障之前发生的写操作,必须被所有 CPU 视为先于屏障之后的写操作发生。
  1. 符号定义$W_a$ 和 $W_b$ 表示两条写操作。
    Wa,  WbW_a, \; W_b
  2. 关系符号$\prec_{\text{sfence}}$ 表示在 sfence 屏障约束下,$W_a$ 排在 $W_b$ 之前。
    Wa≺sfenceWbW_a \prec_{\text{sfence}} W_b
  3. 结果$\Longrightarrow$ 说明最终所有核心观察到的全局顺序必然是 $W_a$ 先、$W_b$ 后,乱序被禁止。

RCU核心思想

你终于受够了“清场式”更新,想出一个革命性做法:书店换新版书时,不去打扰正在读书的顾客。店员把新版书放到柜台,再在公告栏上贴出新版的位置。老顾客抬头取书,可能拿到旧版也可能拿到新版,但无论是哪一本,都是完整可读的。

RCU(Read-Copy-Update,读-复制-更新)的核心思想正是:读端无锁。读者访问共享数据时不需要拿任何锁,直接读取指针即可。写者要更新数据时,先复制一份旧数据,在副本上完成修改,然后用一次原子的指针替换发布新版本,让之后的读者看到新数据。

这里“复制”意味着每次写都产生新实例,写者开销较高、内存短暂翻倍;但换来的是读端零锁、零阻塞、零重试。读者的唯一责任,是进入读临界区时通知系统“我在读书”(rcu_read_lock),退出时声明“读完了”(rcu_read_unlock)。

但这里有个问题:新书已经上架,正读着旧书的顾客还没走,什么时候才能把旧书收走?这就是 RCU 最精妙的部分——宽限期。

Pnew←原子写Pold,读者可能读到 Pold 或 Pnew,但绝不会读到中间状态P_{\text{new}} \xleftarrow{\text{原子写}} P_{\text{old}},\qquad\text{读者可能读到 } P_{\text{old}} \text{ 或 } P_{\text{new}},但绝不会读到中间状态
原子替换让读者要么看到旧版本,要么看到新版本,二者都是完整合法的数据。
  1. 符号定义$P_{\text{old}}$ 表示旧数据指针,$P_{\text{new}}$ 表示新数据指针。
    Pold,  PnewP_{\text{old}}, \; P_{\text{new}}
  2. 操作符号$\xleftarrow{\text{原子写}}$ 表示执行一次不可分割的指针赋值。
    ←原子写\xleftarrow{\text{原子写}}
  3. 安全性读者观察到任何一侧都不算数据竞争,这是 RCU 读端无锁的根基。

RCU宽限期

书店角落,最后一位捧着旧版小说的顾客读得入神。店员已经开始卖新版,却不能立刻把旧书从架上撤下——万一顾客读到一半想翻前面的章节呢?店员只能等,等最后一位旧读者离开,再放心回收。

RCU 把“所有读者都退出读临界区”的这段时间称为宽限期(Grace Period)。内核中,每个 CPU 会定期报告自己是否处于用户态、空闲态或读临界区外——这叫做静止状态(quiescent state)。当所有 CPU 都至少经过一次静止状态,内核就能确认:任何旧读者都已离开,宽限期结束。

宽限期到达后,写者可以安全回收旧数据。synchronize_rcu() 会阻塞写者直到宽限期结束;call_rcu() 则注册一个回调,让内核在宽限期后异步回收。延迟虽高但绝对安全——这就是 RCU 用时间换读端性能的哲学。

宽限期是 RCU 的“代价时刻”:旧数据多活一阵,换来了读者永不等待。现在,我们把三把钥匙摆在一起,看看什么时候该用哪一把。

Tg  ≥  max⁡∀i∈R(tread(i))T_g \;\geq\; \max_{\forall i \in R}\left(t_{\text{read}}^{(i)}\right)
宽限期必须覆盖所有并发读者临界区的结束时刻,才能保证旧数据无人引用。
  1. 符号定义$T_g$ 表示宽限期总时长。
    TgT_g
  2. 集合符号$R$ 表示当前所有在读的读者集合,$i \in R$ 遍历每一个读者。
    ∀i∈R\forall i \in R
  3. 取最大值$t_{\text{read}}^{(i)}$ 取所有读者中退出最晚的那一个,宽限期必须比它更长。

三锁对比选型

你对书店的运营策略已经了然于心:收银台只有两三个人时,让大家站着等(自旋锁);阅读区读者多、店员偶尔整理书架时,读者共享、整理时清场(读写锁);热门书频繁改版、读者络绎不绝时,干脆无锁读、后台换版(RCU)。

三把锁的取舍可以概括为:

  • 自旋锁:临界区极短(微秒级以下)、竞争不激烈时最划算;
  • 读写锁:读多写少、读者需要并发、写者必须独占时最稳;
  • RCU:读操作极其频繁、写操作能容忍复制和延迟回收时最强。

选型还要考虑工程因素:公平性、可重入性、是否需要手动挂内存屏障、锁持有时间是否可预测。Linux 内核路径上三种锁都很常见,性能敏感的代码宁可先选自旋锁或 RCU,也不轻易让线程睡眠。

小书店三套方案并无高下之分,只有合不合适。理解每把锁背后的代价,你才能在真实并发系统里做出清醒的选择——这也是从“会写锁”到“会设计锁”的分水岭。

Lock(workload)={SpinLock,tcs 极短且竞争低RWLock,读远多于写RCU,读极频繁、写可延迟回收\text{Lock}(\text{workload}) =\begin{cases}\text{SpinLock}, & t_{\text{cs}} \text{ 极短且竞争低}\\\text{RWLock}, & \text{读远多于写}\\\text{RCU}, & \text{读极频繁、写可延迟回收}\end{cases}
锁的选择本质是工作负载模式与性能代价的匹配,没有万能锁。
  1. 符号定义$\text{Lock}(\text{workload})$ 表示根据工作负载选择锁类型的决策函数。
    Lock(workload)\text{Lock}(\text{workload})
  2. 分段函数$\begin{cases} \cdots \end{cases}$ 表示三种互斥的判定分支。
    {SpinLocktcs 极短RWLock读多写少RCU读极频繁\begin{cases} \text{SpinLock} & t_{\text{cs}} \text{ 极短}\\ \text{RWLock} & \text{读多写少}\\ \text{RCU} & \text{读极频繁} \end{cases}
  3. 条件说明$t_{\text{cs}}$ 是临界区执行时间,它是区分自旋锁与其他锁的关键指标。

AI 应用场景

Linux 内核 dentry 缓存(RCU 读路径)

阶段:高并发系统的数据访问路径优化阶段

解决的问题:文件系统的路径查找(path lookup)是极高频率的读操作。传统读写锁让每次查询都产生原子计数开销,在数百核服务器上成为瓶颈。RCU 让读路径完全无锁,写者(改名、删除)通过复制-替换与宽限期回收旧 dentry,查询吞吐大幅提升。

具体案例:Linux 内核的 dcache 中,d_lookup 在 RCU 模式下不需要拿锁,可被数百个 CPU 并行调用;内核社区实测,在大型多核服务器上,RCU 路径解析比自旋锁版本性能提升数倍。

数据库缓冲池的读写锁(InnoDB / MySQL)

阶段:存储引擎并发控制与事务调度阶段

解决的问题:buffer pool 的 LRU 链表操作频繁读、偶发写。若用全局互斥锁,所有线程互相等待;读写锁允许多个查询线程同时扫描链表,只有 checkpoint 淘汰页时才获取写锁,显著降低锁竞争。

具体案例:MySQL InnoDB 的 buffer pool 使用 rw_lock 保护 LRU 列表,在高并发只读场景下,读锁共享让事务调度不互相阻塞,避免锁竞争成为 TPS 天花板。

网络路由表更新(RCU 转发平面)

阶段:控制面与转发面的数据同步阶段

解决的问题:路由器转发报文需要极低延迟,读路径绝不接受加锁;而路由更新(如 BGP 变化)频率低但必须完整生效。RCU 让转发平面无锁查表,新路由表通过原子指针替换发布,软中断处理路径无需额外同步。

具体案例:Linux 内核 FIB(转发信息库)使用 RCU 保护路由查找,在满负载转发测试中,RCU 读路径可以接近纯内存读取速度,且路由更新不会造成丢包毛刺。

Python 演示

python125 行
import threadingimport time  class SpinLock:    """自旋锁:忙等待直到拿到锁"""    def __init__(self):        self._locked = False        self._mutex = threading.Lock()     def acquire(self):        while True:            with self._mutex:                if not self._locked:                    self._locked = True                    return     def release(self):        with self._mutex:            self._locked = False  class RWLock:    """读写锁:多个读者共享,写者独占"""    def __init__(self):        self._readers = 0        self._mutex = threading.Lock()        self._wlock = threading.Lock()     def read_acquire(self):        with self._mutex:            self._readers += 1            if self._readers == 1:                self._wlock.acquire()     def read_release(self):        with self._mutex:            self._readers -= 1            if self._readers == 0:                self._wlock.release()     def write_acquire(self):        self._wlock.acquire()     def write_release(self):        self._wlock.release()  class RCUStore:    """RCU模拟:复制-修改-替换,宽限期后回收旧数据"""    def __init__(self, data):        self.data = data        self._readers = 0        self._mutex = threading.Lock()        self._old_data = []     def read_enter(self):        with self._mutex:            self._readers += 1     def read_exit(self):        with self._mutex:            self._readers -= 1            self._reclaim_if_due()     def update(self, new_data):        with self._mutex:            self._old_data.append(self.data)            self.data = new_data            self._reclaim_if_due()     def _reclaim_if_due(self):        if self._readers == 0 and self._old_data:            print('grace period done, reclaim:', self._old_data.pop(0))  def read_worker(lock, name):    lock.read_acquire()    print('reader', name, 'reading')    time.sleep(0.05)    lock.read_release()  def write_worker(lock):    lock.write_acquire()    print('writer updating')    time.sleep(0.1)    lock.write_release()  def rcu_reader(store, name):    store.read_enter()    print('rcu reader', name, 'sees', store.data)    time.sleep(0.05)    store.read_exit()  def rw_demo():    lock = RWLock()    ts = [threading.Thread(target=read_worker, args=(lock, str(i)))          for i in range(3)]    ts.append(threading.Thread(target=write_worker, args=(lock,)))    for t in ts:        t.start()    for t in ts:        t.join()  def rcu_demo():    store = RCUStore('version-1')    rs = [threading.Thread(target=rcu_reader, args=(store, str(i)))          for i in range(3)]    for t in rs:        t.start()    time.sleep(0.01)    store.update('version-2')    for t in rs:        t.join()  if __name__ == '__main__':    print('== RWLock ==')    rw_demo()    print('== RCU ==')    rcu_demo()
目的:用线程调度模拟三种同步机制——自旋锁忙等待、读写锁共享读/独占写、RCU 宽限期回收。
  1. self._readers += 1读者进入时递增计数,多个读者可同时进入。
  2. if self._readers == 1: self._wlock.acquire()第一个读者负责挡住写者,保证共享读。
  3. if self._readers == 0: self._wlock.release()最后一个读者离开后放行写者,写者独占。
  4. self._old_data.append(self.data)写者把旧版本记入回收队列,相当于开始等待宽限期。
  5. if self._readers == 0 and self._old_data:所有读者退出即宽限期结束,此刻才安全回收旧数据。

常见误区

误区自旋锁获取失败时,线程应该去睡一会儿,等锁释放后再被唤醒,就像互斥锁那样,这样最省资源。

正解自旋锁恰恰相反,它不会让线程睡眠,而是在 CPU 上不断循环尝试获取,这正是“自旋”的含义。它用忙等待消除上下文切换开销,只适合临界区极短的场景。若在长临界区使用自旋锁,CPU 空转的浪费远超睡眠唤醒的收益。

为何常见:初学者容易把“等锁”统一想象成“挂起—唤醒”,忽略了自旋锁刻意牺牲 CPU 换低延迟的设计取向。

误区读多写少的时候直接上读写锁就行,肯定比普通互斥锁快,不用纠结。

正解读写锁的读路径也要维护原子计数、判断第一个/最后一个读者,这些开销在临界区极短时可能超过普通锁。而且写者频繁出现时,写者互斥和读者排队的成本会放大,性能可能更差。选型必须结合临界区长度和读写比例实测。

为何常见:把“并发读”理想化,忽略了原子操作和缓存一致性流量也有真实成本。

误区RCU 就是延迟释放旧数据,比如等 1 秒,时间到了就回收,像一个定时器。

正解宽限期是“所有读者都离开读临界区”的那个时刻,不是固定时长。内核通过检测每个 CPU 是否经过静止状态来判定宽限期结束,然后才回收。若提前回收,仍在读旧数据的线程可能访问到已释放内存,直接崩溃。宽限期是状态检测,不是时间设定。

为何常见:只看到“延迟回收”的表象,没有理解宽限期背后的静止状态跟踪机制。

误区RCU 读端无锁,所以整个机制就是零开销,写者也没有代价。

正解读端零锁的代价由写者承担:每次更新都要复制整个数据对象,内存占用翻倍,且必须等待宽限期。此外,读者虽不用拿锁,仍可能需要编译器/CPU 屏障(或至少使用 rcu_dereference)来避免读到撕裂的指针。

为何常见:把“读端无锁”误解为“全文无锁”,忽略了复制、发布、回收一整条链路上的隐性成本。

误区现代 CPU 缓存一致性协议很强,加上锁就不会乱序,内存屏障是多余的优化细节。

正解锁的内部确实自带屏障,但无锁算法、RCU 读端、自定义同步都需要开发者手动放置屏障,否则会出现“代码按序执行、内存却乱序可见”的诡异 bug。尤其在 ARM、RISC-V 等弱内存序架构上,屏障不是可选优化,而是正确性底线。

为何常见:在 x86 上测试一切正常,换到弱内存序架构就翻车,于是误以为是编译器或硬件 bug,实则是屏障缺失。

面试题

Q1. 自旋锁与互斥锁最本质的区别是什么?

单选题 · easy

  • A.自旋锁只能用于多核 CPU,互斥锁只能用于单核
  • B.自旋锁忙等待不切换线程,互斥锁会阻塞切换← 正确答案
  • C.自旋锁速度一定比互斥锁快
  • D.自旋锁允许递归加锁,互斥锁不允许

答案:B。自旋锁获取失败时在 CPU 上自旋(忙等待),不进行上下文切换;互斥锁则让线程睡眠、让出 CPU,等待被唤醒。B 正确。A 错在互斥锁也能用于多核;C 在临界区较长时不成立;D 两者通常都不可递归(视具体实现)。

Q2. 以下哪个场景最适合使用自旋锁?

单选题 · easy

  • A.临界区持有时间达到毫秒级
  • B.临界区极短且竞争不激烈← 正确答案
  • C.读多写少的缓存查找
  • D.需要可重入加锁

答案:B。自旋锁的价值在于用短促忙等避免线程切换开销,临界区必须极短且竞争低。毫秒级临界区自旋空转浪费严重;读多写少适合读写锁;可重入需要特定锁类型。所以选 B。

Q3. 为什么自旋锁在单核 CPU 上通常要配合关闭抢占或中断使用?

单选题 · easy

  • A.因为单核 CPU 不提供原子指令
  • B.因为等锁线程与持锁线程共享同一颗核,空转永远等不到锁← 正确答案
  • C.因为单核 CPU 无法运行多线程
  • D.因为单核必须使用互斥锁

答案:B。单核下同一时刻只有一个线程在跑,持锁线程被抢占后,等锁线程拿着 CPU 自旋,锁永远不会被释放,形成死等。关闭抢占/中断可避免持有者被换走。B 正确。

Q4. 读写锁中的“写者饥饿”问题是指什么?

单选题 · mid

  • A.写者无法获取 CPU 时间片
  • B.读者持续持读锁,写者长期等待写锁← 正确答案
  • C.写者优先级过高导致读者饿死
  • D.写锁释放时没有唤醒读者

答案:B。写者持写锁必须等所有读者退出,若读者源源不断进来,写者可能一直得不到锁,这就是写者饥饿。解决方案是写者优先排队或公平调度。B 正确。

Q5. 关于内存屏障与锁的关系,下列说法正确的是?

多选题 · mid

  • A.锁的实现内部通常包含内存屏障← 正确答案
  • B.内存屏障仅用于无锁编程,锁内不需要
  • C.屏障保证屏障前后的内存操作按序全局可见← 正确答案
  • D.不同 CPU 架构对内存屏障的需求完全相同

答案:AC。A 正确,锁依赖原子指令和屏障保证互斥与可见性;C 正确,屏障防止指令和内存序重排。B 错,锁内部同样依赖屏障;D 错,x86 是强内存序,ARM/RISC-V 是弱内存序,需求不同。

Q6. RCU 与读写锁最核心的区别是什么?

单选题 · mid

  • A.RCU 使用互斥锁保护读端
  • B.RCU 读端无需访问任何锁← 正确答案
  • C.RCU 不允许写操作
  • D.读写锁不允许并发读

答案:B。RCU 的读端无锁,读者直接读取指针,不需要原子计数或锁获取;读写锁的读端虽然共享,但仍需原子维护读者计数。B 正确。

Q7. RCU 宽限期结束的决定因素是什么?

单选题 · mid

  • A.一个固定的时间常量
  • B.所有 CPU 都至少经历一次静止状态← 正确答案
  • C.写者数量达到某个阈值
  • D.内存屏障执行完成

答案:B。宽限期结束的标志是所有 CPU 都经过静止状态(用户态、空闲或不在读临界区),此时可以确认旧读者都已退出。所谓静止状态即 quiescent state。B 正确。

Q8. RCU 写者更新数据时,以下哪些操作是必要的?

多选题 · hard

  • A.复制旧数据生成新副本← 正确答案
  • B.在副本上完成修改← 正确答案
  • C.原子替换指针发布新版本← 正确答案
  • D.立即释放旧数据内存

答案:ABC。RCU 写端三步:复制、修改、原子替换发布。旧数据必须等待宽限期结束后才能释放,不能立即释放(D 错),否则可能被尚未退出的读者访问到野指针。

Q9. 在 Linux 内核中,路径查找(dcache)使用 RCU 而非常规锁的主要原因是?

单选题 · hard

  • A.路径查找写操作非常频繁
  • B.路径查找是极高频读操作,需要读端零锁← 正确答案
  • C.自旋锁无法保护链表
  • D.RCU 实现代码量最少

答案:B。dentry 查找是极高频只读操作,RCU 让读端无锁并行,写操作(改名、删除)通过复制-替换与宽限期回收。B 正确,A 描述的写频繁场景恰是 RCU 的弱项。

Q10. 关于自旋锁、读写锁与 RCU 的选型,正确的判断是?

多选题 · extreme

  • A.临界区极短且写较多时,自旋锁通常优于读写锁← 正确答案
  • B.读多写少且读需要高并发时,读写锁是稳定选择← 正确答案
  • C.RCU 写者需要等待宽限期,写延迟高于读写锁写者← 正确答案
  • D.任何读多写少场景都应首选 RCU

答案:ABC。A 对,短临界区下自旋锁无切换开销;B 对,读多写少正是读写锁的适用区;C 对,RCU 写者要复制对象并等宽限期,是“读优写忍”策略;D 错,RCU 要求数据可复制替换、写后能容忍延迟回收,不是万能首选。