想象你经营一家小书店。店里只有一个收银台,顾客买书得排队。这时你会遇到三种情况,正好对应并发编程里的三把“锁”。 第一种,排队的人不多,大家都站在收银台前盯着店员找钱——这就是自旋锁:等待的人不离开,在原地打转(自旋),店员刚喊“下一位”,顾客立刻接上。 第二种,书店里开了一块阅读区,很多顾客可以同时看书;但店员要整理书架时,必须请所有读者放下书离开。这就是读写锁:读者之间互不干扰,写者独占禁区。 第三种,书店更新书目。店员不需要喊“所有人放下书”,而是把新版书挂到公告栏上,老顾客继续读完手里的旧版,新顾客直接拿新版;店员等最后一个读旧版的顾客离开,再把旧书收走。这就是 RCU。 电脑里,无数线程就像这些顾客,争抢着同一份数据。选错锁,好比让顾客在收银台前干等一小时,或让所有看书的人反复被打断。这篇文章,就用这间小书店,带你摸清三种锁的脾气、毛病,以及什么时候该用哪一把。
这套知识是理解 Linux 内核、数据库引擎和高并发中间件的底层钥匙,掌握它,你才能在系统设计面试和性能调优中真正看穿并发瓶颈。
可视化演示
通过自旋锁忙等待、读写锁并发控制和RCU宽限期回收三个模式,对比三种内核同步机制的适用场景与性能特征。
本节配有交互动效(进入视口后自动加载,约 3KB)。
自旋锁忙等待机制 多核竞争同一锁,未获锁的CPU持续忙等(自旋)不睡眠 适用于临界区极短场景,避免上下文切换开销 while(!try_lock()) { /* spin */ } // busy-wait, no sleep 自旋锁在等待时不释放CPU,临界区必须极短以免浪费CPU周期
自旋锁原理
收银台前只有两三位顾客时,你会怎么做?站着等。眼睛盯着店员找钱,手随时准备把书递上去,店员刚说完“下一位”,你立刻接棒。没有离开,没有绕一圈再回来——这就是“自旋”:人还在原地,注意力始终锁定目标。
操作系统里的线程竞争共享变量时,处境完全一样。线程 A 进入临界区前要先拿锁,其他线程只能等着。自旋锁的等待方式很特别:它们不睡觉、不让出 CPU,而是在一个循环里反复尝试获取锁。这种“原地打转”的等待,学名叫忙等待(busy-waiting)。
自旋锁能成立,靠的是硬件原子指令,如 test-and-set、xchg。它们在 CPU 层面把“读旧值—改新值—写回”三步合成一步,中间不会被打断。拿到锁的线程在临界区执行完立即释放,等锁线程几乎零延迟补位,省掉了上下文切换(context switch)的开销——一次切换动辄几十到几百纳秒,而自旋一轮往往只要几纳秒。
回到书店,收银台前排着两三个人时,站着等确实比“先走开、过会儿再来”快得多。可排队的人一旦多起来,问题就来了——下一节,我们看看自旋锁的困境。
- 符号定义$t_{\text{spin}}$ 表示线程自旋等待并成功进入临界区的总耗时。
- 符号定义$t_{\text{switch}}$ 表示一次上下文切换(保存/恢复线程状态)的固定开销。
- 判据含义当 $t_{\text{spin}} < t_{\text{switch}}$ 时,与其让线程休眠切换,不如让它原地自旋,用更短的时间换取更低的延迟。
自旋锁问题
促销日,收银台前排起长龙。你还敢站在台前干等吗?每个人都原地转圈、跺脚,整个店堂挤成一团,谁也没法去干别的,后面的人甚至挤不进来。你突然意识到:等待本身也在消耗精力。
自旋锁最大的毛病正是空转烧 CPU。等待的线程不睡眠,占着处理器不断轮询,白白消耗算力。等锁线程越多,浪费越严重。它还不公平:后来的线程可能先抢到锁,先来的反而一直抢不到——理论上甚至存在饥饿风险。此外,自旋锁要求临界区极短:临界区一旦拖长,自旋等待的成本就会反超上下文切换,得不偿失。
单核 CPU 上问题更尖锐:拿着锁的线程被抢占后,等锁线程在同一颗核上空转,永远也等不到锁被释放,变成死等。因此自旋锁在单核上必须配合关闭抢占或中断使用,确保持锁线程能一路走完临界区。
小书店的教训是:自旋锁只适合“人少、结账快”的时刻。当家大了、需求复杂了,就得设计更精细的排队规则——读写锁登场。
- 符号定义$n_{\text{waiter}}$ 表示当前排队自旋的等待线程数量。
- 符号定义$t_{\text{spin}}$ 表示每个等锁线程单次自旋尝试的耗时。
- 乘积含义总浪费时间等于等待线程数与自旋耗时的乘积,说明等的人越多,浪费线性放大。
读写锁设计
书店扩张,你开辟了一块阅读区。看书的顾客之间互不打扰,几十个人能同时坐着翻书——“读”是共享的。但店员要往书架上补新书、改价签时,必须先请所有读者放下书离开,整理完成再开放——“写”是独占的。
读写锁(Reader-Writer Lock)正是这个逻辑。它把加锁分成两种:读锁和写锁。任意多个线程可以同时持有读锁;而写锁同时只能有一个线程持有,并且与其他任何读锁或写锁互斥。核心结构是一个“读者计数 + 写锁标志”的组合。
实现上,读者获取读锁时把读者计数加 1;如果自己是第一位读者,还要尝试获取底层的那把“大锁”。写者必须独占这把大锁。这意味着,当有写者在排队时,新来的读者会被挡在门外,以免读者源源不断涌入、写者永远轮不到。这种设计天生适合缓存、配置中心这类读多写少的场景。
阅读区虽然热闹,却埋下一个隐患:店员总来整理书架,读者会被反复清场;读者太多,整理书架的店员可能永远轮不到动手。这就是下一节要谈的公平性。
- 符号定义$t_{r_1}, t_{r_2}, \ldots, t_{r_n}$ 表示 n 个读者各自在临界区内的停留时间。
- 取最大值$\max$ 表示取序列中最大的一个。
- 物理含义由于读锁共享,所有读者同时执行,总耗时才等效于“最慢读者”,这正是读写锁提升并发度的数学来源。
锁的公平性
你发现阅读区的奇怪现象:店员想整理书架,读者却一个接一个进来,眼看永远轮不到店员;反过来,店员频繁清场,读者又抱怨连一页书都看不完。到底谁先来、谁后到?这就是并发世界的公平性(Fairness)问题。
计算机里,读写锁面临典型的写者饥饿:读者不断申请读锁并共享进入,写者却一直等不到写锁。解决思路是让先来者先服务。比如排队票号锁(Ticket Lock):每个线程领一张票,号码到了才能进;读写锁内部也可以维护等待队列,让写者排在读者前面,保证写者不被读者无限压制。
公平性不是开关,而是一组权衡。写者优先会略微牺牲读者的并发度;读者优先则可能让写者饿死;理想方案是“先来后到、按序放行”。Linux 内核的 rwsem 就采用写者优先策略,避免系统长期停留在只读状态。
排队规则定得再好,也挡不住一个底层问题:即使线程按顺序执行,我们看到的“先后”一定真实吗?CPU 乱序执行和缓存机制,会让代码顺序与真实发生顺序不一致。要解决这个问题,得请出内存屏障。
- 符号定义$t_w^{(k+1)}$ 表示第 k+1 个排队者从排队到进入临界区的等待时长。
- 累加符号$\sum_{i=1}^{k} t_{c_i}$ 表示把前 k 个线程各自占用的临界区时间加起来。
- 含义前面任何线程的临界区变长,都会直接推高后面所有人的等待时间,这就是不公平感被放大的根源。
内存屏障
收银台前,店员照单据叫号,顾客却发现:明明先来的人先付款,系统里记录的却是后付款的人先完成。原因在于“执行顺序”和“记账顺序”被悄悄调换了。为了杜绝混乱,书店规定:所有收据必须盖章,按章号顺序入账。
CPU 为了提速,会乱序执行指令(Out-of-Order Execution),还会把写操作暂存在自己的 store buffer 里,导致其他核心看不到最新值。锁的实现必须配合内存屏障(Memory Barrier),它像一道盖章关卡,强制屏障前后的内存操作按序全局可见。
具体来说,sfence 保证屏障前的写操作先于屏障后的写操作被其他核看到;lfence 保证读操作不被重排;mfence 同时约束读写。Linux 内核用 smp_mb() 等宏在需要处插入屏障。自旋锁、读写锁内部都藏着屏障——这也是锁除互斥外最重要的职责:提供内存可见性。
有了屏障,原子操作才真正“像盖章一样”算数。但所有锁多少都要阻塞或等待,能不能让读端彻底无锁?RCU 给出了一个不一样的答案。
- 符号定义$W_a$ 和 $W_b$ 表示两条写操作。
- 关系符号$\prec_{\text{sfence}}$ 表示在 sfence 屏障约束下,$W_a$ 排在 $W_b$ 之前。
- 结果$\Longrightarrow$ 说明最终所有核心观察到的全局顺序必然是 $W_a$ 先、$W_b$ 后,乱序被禁止。
RCU核心思想
你终于受够了“清场式”更新,想出一个革命性做法:书店换新版书时,不去打扰正在读书的顾客。店员把新版书放到柜台,再在公告栏上贴出新版的位置。老顾客抬头取书,可能拿到旧版也可能拿到新版,但无论是哪一本,都是完整可读的。
RCU(Read-Copy-Update,读-复制-更新)的核心思想正是:读端无锁。读者访问共享数据时不需要拿任何锁,直接读取指针即可。写者要更新数据时,先复制一份旧数据,在副本上完成修改,然后用一次原子的指针替换发布新版本,让之后的读者看到新数据。
这里“复制”意味着每次写都产生新实例,写者开销较高、内存短暂翻倍;但换来的是读端零锁、零阻塞、零重试。读者的唯一责任,是进入读临界区时通知系统“我在读书”(rcu_read_lock),退出时声明“读完了”(rcu_read_unlock)。
但这里有个问题:新书已经上架,正读着旧书的顾客还没走,什么时候才能把旧书收走?这就是 RCU 最精妙的部分——宽限期。
- 符号定义$P_{\text{old}}$ 表示旧数据指针,$P_{\text{new}}$ 表示新数据指针。
- 操作符号$\xleftarrow{\text{原子写}}$ 表示执行一次不可分割的指针赋值。
- 安全性读者观察到任何一侧都不算数据竞争,这是 RCU 读端无锁的根基。
RCU宽限期
书店角落,最后一位捧着旧版小说的顾客读得入神。店员已经开始卖新版,却不能立刻把旧书从架上撤下——万一顾客读到一半想翻前面的章节呢?店员只能等,等最后一位旧读者离开,再放心回收。
RCU 把“所有读者都退出读临界区”的这段时间称为宽限期(Grace Period)。内核中,每个 CPU 会定期报告自己是否处于用户态、空闲态或读临界区外——这叫做静止状态(quiescent state)。当所有 CPU 都至少经过一次静止状态,内核就能确认:任何旧读者都已离开,宽限期结束。
宽限期到达后,写者可以安全回收旧数据。synchronize_rcu() 会阻塞写者直到宽限期结束;call_rcu() 则注册一个回调,让内核在宽限期后异步回收。延迟虽高但绝对安全——这就是 RCU 用时间换读端性能的哲学。
宽限期是 RCU 的“代价时刻”:旧数据多活一阵,换来了读者永不等待。现在,我们把三把钥匙摆在一起,看看什么时候该用哪一把。
- 符号定义$T_g$ 表示宽限期总时长。
- 集合符号$R$ 表示当前所有在读的读者集合,$i \in R$ 遍历每一个读者。
- 取最大值$t_{\text{read}}^{(i)}$ 取所有读者中退出最晚的那一个,宽限期必须比它更长。
三锁对比选型
你对书店的运营策略已经了然于心:收银台只有两三个人时,让大家站着等(自旋锁);阅读区读者多、店员偶尔整理书架时,读者共享、整理时清场(读写锁);热门书频繁改版、读者络绎不绝时,干脆无锁读、后台换版(RCU)。
三把锁的取舍可以概括为:
- 自旋锁:临界区极短(微秒级以下)、竞争不激烈时最划算;
- 读写锁:读多写少、读者需要并发、写者必须独占时最稳;
- RCU:读操作极其频繁、写操作能容忍复制和延迟回收时最强。
选型还要考虑工程因素:公平性、可重入性、是否需要手动挂内存屏障、锁持有时间是否可预测。Linux 内核路径上三种锁都很常见,性能敏感的代码宁可先选自旋锁或 RCU,也不轻易让线程睡眠。
小书店三套方案并无高下之分,只有合不合适。理解每把锁背后的代价,你才能在真实并发系统里做出清醒的选择——这也是从“会写锁”到“会设计锁”的分水岭。
- 符号定义$\text{Lock}(\text{workload})$ 表示根据工作负载选择锁类型的决策函数。
- 分段函数$\begin{cases} \cdots \end{cases}$ 表示三种互斥的判定分支。
- 条件说明$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 演示
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()
self._readers += 1读者进入时递增计数,多个读者可同时进入。if self._readers == 1: self._wlock.acquire()第一个读者负责挡住写者,保证共享读。if self._readers == 0: self._wlock.release()最后一个读者离开后放行写者,写者独占。self._old_data.append(self.data)写者把旧版本记入回收队列,相当于开始等待宽限期。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. 自旋锁与互斥锁最本质的区别是什么?
- A.自旋锁只能用于多核 CPU,互斥锁只能用于单核
- B.自旋锁忙等待不切换线程,互斥锁会阻塞切换← 正确答案
- C.自旋锁速度一定比互斥锁快
- D.自旋锁允许递归加锁,互斥锁不允许
答案:B。自旋锁获取失败时在 CPU 上自旋(忙等待),不进行上下文切换;互斥锁则让线程睡眠、让出 CPU,等待被唤醒。B 正确。A 错在互斥锁也能用于多核;C 在临界区较长时不成立;D 两者通常都不可递归(视具体实现)。
Q2. 以下哪个场景最适合使用自旋锁?
- A.临界区持有时间达到毫秒级
- B.临界区极短且竞争不激烈← 正确答案
- C.读多写少的缓存查找
- D.需要可重入加锁
答案:B。自旋锁的价值在于用短促忙等避免线程切换开销,临界区必须极短且竞争低。毫秒级临界区自旋空转浪费严重;读多写少适合读写锁;可重入需要特定锁类型。所以选 B。
Q3. 为什么自旋锁在单核 CPU 上通常要配合关闭抢占或中断使用?
- A.因为单核 CPU 不提供原子指令
- B.因为等锁线程与持锁线程共享同一颗核,空转永远等不到锁← 正确答案
- C.因为单核 CPU 无法运行多线程
- D.因为单核必须使用互斥锁
答案:B。单核下同一时刻只有一个线程在跑,持锁线程被抢占后,等锁线程拿着 CPU 自旋,锁永远不会被释放,形成死等。关闭抢占/中断可避免持有者被换走。B 正确。
Q4. 读写锁中的“写者饥饿”问题是指什么?
- A.写者无法获取 CPU 时间片
- B.读者持续持读锁,写者长期等待写锁← 正确答案
- C.写者优先级过高导致读者饿死
- D.写锁释放时没有唤醒读者
答案:B。写者持写锁必须等所有读者退出,若读者源源不断进来,写者可能一直得不到锁,这就是写者饥饿。解决方案是写者优先排队或公平调度。B 正确。
Q5. 关于内存屏障与锁的关系,下列说法正确的是?
- A.锁的实现内部通常包含内存屏障← 正确答案
- B.内存屏障仅用于无锁编程,锁内不需要
- C.屏障保证屏障前后的内存操作按序全局可见← 正确答案
- D.不同 CPU 架构对内存屏障的需求完全相同
答案:AC。A 正确,锁依赖原子指令和屏障保证互斥与可见性;C 正确,屏障防止指令和内存序重排。B 错,锁内部同样依赖屏障;D 错,x86 是强内存序,ARM/RISC-V 是弱内存序,需求不同。
Q6. RCU 与读写锁最核心的区别是什么?
- A.RCU 使用互斥锁保护读端
- B.RCU 读端无需访问任何锁← 正确答案
- C.RCU 不允许写操作
- D.读写锁不允许并发读
答案:B。RCU 的读端无锁,读者直接读取指针,不需要原子计数或锁获取;读写锁的读端虽然共享,但仍需原子维护读者计数。B 正确。
Q7. RCU 宽限期结束的决定因素是什么?
- A.一个固定的时间常量
- B.所有 CPU 都至少经历一次静止状态← 正确答案
- C.写者数量达到某个阈值
- D.内存屏障执行完成
答案:B。宽限期结束的标志是所有 CPU 都经过静止状态(用户态、空闲或不在读临界区),此时可以确认旧读者都已退出。所谓静止状态即 quiescent state。B 正确。
Q8. RCU 写者更新数据时,以下哪些操作是必要的?
- A.复制旧数据生成新副本← 正确答案
- B.在副本上完成修改← 正确答案
- C.原子替换指针发布新版本← 正确答案
- D.立即释放旧数据内存
答案:ABC。RCU 写端三步:复制、修改、原子替换发布。旧数据必须等待宽限期结束后才能释放,不能立即释放(D 错),否则可能被尚未退出的读者访问到野指针。
Q9. 在 Linux 内核中,路径查找(dcache)使用 RCU 而非常规锁的主要原因是?
- A.路径查找写操作非常频繁
- B.路径查找是极高频读操作,需要读端零锁← 正确答案
- C.自旋锁无法保护链表
- D.RCU 实现代码量最少
答案:B。dentry 查找是极高频只读操作,RCU 让读端无锁并行,写操作(改名、删除)通过复制-替换与宽限期回收。B 正确,A 描述的写频繁场景恰是 RCU 的弱项。
Q10. 关于自旋锁、读写锁与 RCU 的选型,正确的判断是?
- A.临界区极短且写较多时,自旋锁通常优于读写锁← 正确答案
- B.读多写少且读需要高并发时,读写锁是稳定选择← 正确答案
- C.RCU 写者需要等待宽限期,写延迟高于读写锁写者← 正确答案
- D.任何读多写少场景都应首选 RCU
答案:ABC。A 对,短临界区下自旋锁无切换开销;B 对,读多写少正是读写锁的适用区;C 对,RCU 写者要复制对象并等宽限期,是“读优写忍”策略;D 错,RCU 要求数据可复制替换、写后能容忍延迟回收,不是万能首选。