想象一家热闹的后厨:几位厨师(线程)同时忙碌,共享同一口炒锅、同一本菜谱、同一台调料车。如果两个厨师同时往锅里倒不同的调料,这锅菜就毁了;如果有人正翻着菜谱,主厨却悄悄把整页菜换掉,也会乱套。传统语言里,这些混乱往往要等到运行时才爆发,甚至只在特定时序下偶现,排查起来像大海捞针。而 Rust 交给你的,是一位极其较真的“后厨督导”——编译器。在开火之前,它就把后厨的规则审了一遍:哪些食材能安全递到别人手上(Send),哪些能摆在公共台上共享(Sync),谁该排队等炒勺(Mutex),谁适合翻菜谱(RwLock),食材存货怎么记数(Arc)。只要规则不合法,编译直接报错,根本轮不到运行时出错。这篇文章就用这座后厨的故事,带你彻底搞懂 Rust 的并发安全模型。
对 AI 从业者来说,Rust 并发安全是构建高吞吐推理服务、向量检索和 MLOps 基础设施的底气——把数据竞争从“线上事故”变成“编译报错”,让你把精力留给算法,而不是半夜查并发 bug。
可视化演示
展示Send/Sync trait编译期防护、Mutex/RwLock锁机制对比、Arc引用计数与死锁预防,理解Rust并发安全的类型系统保障。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Send与Sync trait编译期防护 Send允许所有权跨线程转移 Sync允许多线程共享不可变引用 T: Send → safe move across threads 编译期阻止数据竞争
数据竞争根源
年三十的后厨,两位厨师同时往同一口锅里下料。一个手快放了盐,另一个紧跟着放了糖——最后端上桌的菜咸甜交加,谁也不知道这是谁的手笔。这放在计算机世界里,就是数据竞争(Data Race):两个线程同时读写同一块内存,至少有一个是写操作,而且彼此没有任何先后顺序。竞争的结果全看运气,同样的代码这次跑得对、下次就翻车,是并发程序里最臭名昭著的“幽灵 bug”。
传统语言(比如 C++)面对数据竞争,只能靠“小心谨慎 + 事后检测工具”。Rust 换了一条路:把并发安全检查从运行时搬到编译期。后厨督导(编译器)在开工前逐一盘问每一块共享内存:这块内存是谁的?谁在用?有没有同步机制?只要有一处说不清,抱歉,编译不通过,后厨根本开不了张。
数据竞争有三个必要条件:多个线程访问同一地址、至少一个写操作、线程间没有同步(锁或原子操作)。Rust 的所有权系统从根上限制了前两条:一块内存只能有一个所有者,想给别人用,要么“连袋子递过去”(所有权转移),要么“借阅”(引用),而且借阅规则——一个可变引用或多个只读引用——由编译器在编译期死死盯住。于是数据竞争从“运行时炸弹”变成了“编译期红字”。
督导员合上小本本:没有许可的共享、没有同步的写入,一律不许开工。这就是 Rust 并发安全的第一道防线,也是后面所有概念共同的地基。
- 共享访问两个及以上线程访问同一个内存地址,
\text{共享访问} - 至少一个写访问中至少存在一个写操作,
\exists \text{写操作} - 无同步访问之间没有锁或原子操作建立先后顺序,
\text{无同步} - 三者缺一不可三条同时满足才构成数据竞争,
\Rightarrow \text{数据竞争}
Send所有权转移
后厨里传递食材有两种方式:一种是“连袋子递过去”——我把整袋面粉交接给你,自己不再留任何一份,这叫所有权转移;另一种是“放公共台子上大家一起取用”。Send trait 管的就是第一种:一个类型能不能被安全地从线程 1 “搬家”到线程 2。
在 Rust 里,跨线程传数据最典型的动作是 thread::spawn 的闭包捕获和 channel 发送,本质都是把一个值的所有权从一个线程交给另一个线程。Rust 规定:被转移值的类型必须实现 Send。为什么要有这样的要求?因为有些类型内部藏着线程私有的东西(比如裸指针、指向特定线程栈的句柄),如果像普通食材一样硬塞给别的线程,对方拿在手里根本没法安全使用。
Send 是一个 marker trait(标记特征),本身没有任何方法,纯粹是给编译器贴的“通行证”标签。它的巧妙之处在于自动推导:编译器逐个检查类型的字段,只要所有字段都是 Send,该类型就自动实现 Send,不需要你写一行代码。被显式排除的典型是 Rc<T>——它的引用计数用了非原子操作,如果被搬到另一个线程,两个线程同时增减计数,内存就毁于一旦,所以编译器直接给它发一张“禁止搬运”证(!Send)。
督导员在包装袋上盖章:“此食材可跨厨房运输”;标着“仅限本厨房”(!Send)的,决不允许递出大门。正是这张标签,保证了所有权的每一次跨线程搬家都安全可控。
- 检查类型 T编译器要判断自定义类型 T 能否跨线程转移,
T : \text{Send} - 遍历全部字段逐个检查 T 的所有字段 f,
\forall\, f \in \text{fields}(T) - 字段必须可发送每个字段也要满足 Send,
f : \text{Send} - 自动推导结论字段全通过则 T 自动实现 Send,
\Rightarrow T : \text{Send}
Sync安全共享
公共调料台上,酱油瓶被所有厨师随手拿起、翻看、放下——但谁也不能擅自改标签。这就是 Sync trait 管的场景:一个类型的“只读借用”(&T)能不能被多个线程同时安全地共享。仔细想想,围观本身也有风险:如果瓶子里装的是“看似安定、实则易燃”的东西,围观群众一多就容易出事。
Sync 和 Send 是孪生兄弟:Send 管“把东西搬过去”,Sync 管“把门打开让大家一起看”。两者之间有一条精确的桥梁:T: Sync 当且仅当 &T: Send。也就是说,如果“指向 T 的只读引用”能被安全地跨线程传递(相当于人手一份借阅证),那 T 就是 Sync 的。这条桥梁把“共享”问题巧妙地化归为“转移”问题,让编译器用同一套所有权机制来守护。
为什么“只读引用”也可能有风险?因为类型内部若藏了“内部可变性”(如 Cell),表面上拿 &T 不能改,内部却偷偷改,两个线程同时改还是会打架。Rust 的解法是把这些类型分门别类:需要同步的内部可变类型(如 Mutex<T>)自己用锁保护,所以能实现 Sync;不需要跨线程的(如 Cell<T>)就只实现 Send 不实现 Sync。于是“共享只读引用+内部同步”的组合,让多线程并发读成为安全常态。
菜谱摆上公共书架,人人都能翻,改动必须过主厨那一关(锁)。Rust 用 Sync 告诉世界:围观很安全,因为任何想动笔的人都得先排队拿笔。
- 只读引用
&T是“借阅证”,持证只能读不能改,\&T - 引用可发送借阅证能安全发给其他线程,
\&T : \text{Send} - 等价定义能发借阅证的 T 就是 Sync,
T : \text{Sync} - 特例印证
Mutex<T>靠内部锁保护,同时是 Send 和 Sync,\text{Mutex<T>} : \text{Send} + \text{Sync}
Mutex互斥锁
后厨里有一把祖传炒勺,珍贵到必须“谁拿到谁用”。Mutex(互斥锁)就是这把炒勺的使用规则:同一时刻只能有一个厨师掌勺,其他厨师只能在门口排队等着,绝不能两个人同时伸手。
在 Rust 中,Mutex<T> 把数据 T 包进一个“专人专柜”。你想碰里面的数据,必须先调用 lock() 取得“钥匙”。如果柜子正被别人用着,lock() 会阻塞等待——线程睡过去、把 CPU 让出来,而不是原地打转空耗。拿到锁后返回的 MutexGuard 是临时的“柜门把手”:只有通过它才能读写数据,而且当它离开作用域(被 drop)时,锁会自动归还。
这正是 Rust 擅长的 RAII(资源获取即初始化)思想:解锁不需要手动调用,MutexGuard 一走出花括号就自动释放,彻底消灭了“忘记 unlock”这类经典 bug。使用时还要留意两个坑:一是中毒机制——如果持锁线程 panic,锁会进入 poisoned 状态,后续 lock() 虽然能拿到锁但会返回 PoisonError,提醒你数据可能处于半更新状态;二是锁粒度——持锁期间不要做慢操作(IO、网络请求),否则所有线程排长队,吞吐量直接雪崩。
炒勺离开厨师的手,才轮到下一位。Mutex 的优势就是简单可靠:不管多少人抢,同一时刻只有一个人能动手。守卫对象像贴身保镖一样盯着,走到哪跟到哪,出门就还钥匙。
lock() 就是问“柜子开着吗”——开着就放行,关着就排队等着;而守卫对象一走出作用域,柜门自动重开。- 申请锁线程调用
lock(m)请求进入临界区,\text{lock}(m) - 分支判断空闲则放行,占用则阻塞等待,
\text{state} = \text{free} / \text{locked} - 守卫返回lock 返回
MutexGuard,操作数据必须通过它,\text{Guard} = \text{lock}(m) - 自动释放Guard 被 drop 时锁状态恢复空闲,
\text{drop}(\text{Guard}) \Rightarrow \text{free}
RwLock读写锁
镇店菜谱太厚了,总不能一人看的时候别人都干等。于是后厨立了新规矩:菜谱随便翻(读),谁都能同时看;但改动必须主厨独占(写)。这套“读共享、写独占”的规则,就是 RwLock(读写锁)。
RwLock<T> 把访问分为两种:read() 得到的读锁允许多个线程同时进入;write() 得到的写锁则独占整个数据区。它天然服务“读多写少”的场景——比如 AI 服务的模型参数表、配置项,每秒被读几万次,写可能几分钟才一次。用 RwLock,读线程之间完全不互斥,吞吐量比 Mutex 高出一大截。
但天下没有免费的午餐。RwLock 内部要维护“当前读者数+等待队列”,加锁解锁的开销比 Mutex 更大。如果读写比例接近 1:1,RwLock 的额外开销反而拖慢速度,此时 Mutex 更划算。还要注意写者优先策略:一旦有写者在排队,新来的读者会被挡在门外,防止写者被源源不断的读者“饿死”——这是公平性设计,但也意味着读者可能偶发多等一会儿。
“菜谱随便翻,改谱要排队。” 选锁就像选厨房规则:看得人多、改得人少,读写锁才能让后厨流水线畅通无阻;如果读写一半一半,还是老老实实回到一把炒勺(Mutex)更省心。
- 读读并行多个 read() 之间互不阻塞,
\text{read} \parallel \text{read} - 写写独占write() 时刻只允许一个线程,
\text{write} \Rightarrow \text{exclusive} - 读写互斥读者与写者不能同时存在,
\text{read} \wedge \text{write} \Rightarrow \text{block} - 写者优先写者排队时新读者让路防饿死,
\text{writer waiting} \Rightarrow \text{no new readers}
Arc共享与克隆
存货登记簿挂在大厅:每位厨师取用共享食材前先登记“我拿了一份”,用完再划掉。只有当登记数为零时,库房才敢把食材清掉。这个登记簿,就是 Arc(原子引用计数)。
之前说过每个数据只能有一个所有者,那现实中多个线程确实都要“各持有一份访问权”怎么办?Arc<T> 应运而生:它的全称是“原子引用计数”(Atomically Reference Counted)。每次 clone() 一份新“权”,计数器 +1;每份 Arc 变量被 drop,计数器 -1;计数器归零那一刻,底层数据才真正被释放。于是多把钥匙指向同一扇门,而门只会在最后一个人离开时关闭一次。
注意一个经典误区:Arc<T> 只解决“共享所有权”,不解决“并发修改”。两个线程拿着同一个 Arc 里的数据,依然不能同时改——这就需要黄金搭档 Arc<Mutex<T>>:Arc 让每个线程都持有访问权,Mutex 保证同一时刻只有一个线程能真正动手。另外,Arc 的计数是原子操作,会带来少量性能开销(缓存一致性代价),所以单线程场景请用轻量的 Rc<T>;还要避免循环引用导致计数器永远不为零,造成内存泄漏。
“先登记再取料,人走账清。” Arc 让共享食材的管理井井有条——人手一份钥匙,但进门还得过炒勺锁(Mutex),共享和安全在 Rust 里从不矛盾。
- 克隆加一
Arc::clone()让引用计数加 1,\text{count} \mathrel{+}= 1 - 释放减一Arc 变量离开作用域时计数减 1,
\text{count} \mathrel{-}= 1 - 归零释放计数为 0 说明无人使用,可安全释放底层数据,
\text{count} = 0 \Rightarrow \text{free} - 黄金组合
Arc<Mutex<T>>同时具备“共享所有权”和“互斥访问”,\text{Arc} \circ \text{Mutex}
死锁与锁策略
最尴尬的后厨事故:厨师 A 握着炒勺不放,等 B 把锅递过来;厨师 B 握着锅不放,等 A 把勺递过去。两人谁也不撒手,整个后厨彻底瘫痪。这就是死锁(Deadlock)——多线程相互等待对方手里的资源,谁也前进不了。
死锁的四个必要条件被称为 Coffman 条件:互斥、持有并等待、不可抢占、循环等待。注意,Rust 不会自动替你预防死锁——lock() 的语义就是“等”,等就可能死等。真正的解法在“做事策略”上:所有线程按相同的顺序获取多把锁(先勺后锅,人人都照此办理),循环等待就被自然打破;或者用 try_lock() 抢锁,抢不到立刻返回错误,先去干别的,绝不死等。
try_lock() 是高性能并发的重要工具:它返回 Result,锁被占用就立刻返回 Err,线程可以做别的工作后再回来尝试。此外还要权衡锁粒度:锁保护的数据越小、持锁时间越短,并发度越高,但锁数量变多、管理变复杂;锁粒度太大则退化成串行。高吞吐服务的经验法则是“细锁+短持锁+一致的锁顺序”。
后厨把“先拿勺再拿锅”写进员工守则,再配上“等不到就先去备菜”(try_lock)的灵活策略,死锁就再也找不到生根的土壤。锁的粒度就像切菜:切得越细,越多人能同时上手,但对刀法的要求也越高。
- 互斥每把锁同时只能被一个线程持有,
\text{互斥} - 持有并等待占着锁还想再拿其他锁,
\text{持有并等待} - 不可抢占持锁线程没主动释放前,别人抢不走,
\text{不可抢占} - 循环等待线程之间互相等待形成环,
\text{循环等待} - 破局之法统一加锁顺序可消除循环等待,
\text{顺序加锁} \Rightarrow \lnot \text{循环}
AI 应用场景
高并发 AI 推理网关的请求计数
阶段:并发服务开发
解决的问题:传统方式用共享变量统计请求数,多线程同时读改写时容易因数据竞争导致计数“少算”;AI 推理服务往往有几十上百个 worker 同时处理请求,精确的 QPS 和 token 用量是计费与监控的命根子。用 Mutex<Metrics> 保护计数器,相当于给炒勺上了锁,任何时刻只有一个线程在记账。
具体案例:某 LLM 推理网关用 Rust 开发,每个请求进来更新 Mutex<Metrics> 中的请求数、token 数,多个线程安全累加。上线后 Prometheus 拉取的指标不再出现“少算”,原本偶发的负数统计彻底消失。
向量数据库的索引热更新
阶段:数据基础设施
解决的问题:embedding 索引在服务运行中需要合并新增向量,读请求极多、写的频率低,正是 RwLock 的用武之地;相比 Mutex,读吞吐可提升数倍,检索延迟也更稳定。
具体案例:一个基于 Rust 的向量检索服务,把索引放在 Arc<RwLock<Index>> 中,检索线程拿读锁并发查询,后台合并任务偶尔拿写锁批量重建索引。新向量入库无需停服,检索永远在线。
MLOps 平台的多线程配置共享
阶段:MLOps 工程
解决的问题:多个训练/推理 worker 共享实验配置,配置项偶尔由平台下发更新。用 Arc<RwLock<Config>> 实现“读多写少”的共享,既避免配置变更时重启服务,又保证任何时刻读到的是完整一致的配置快照。
具体案例:MLOps 平台在训练集群中下发超参数,每个 worker 保存一份 Arc<RwLock<Config>>,巡检线程定期探测平台新配置,获得写锁后原子替换配置字典;训练线程读锁读取,全程无锁冲突、无数据撕裂。
Python 演示
# -*- coding: utf-8 -*-"""用 Python 模拟 Rust 并发安全的核心思想:数据竞争与锁""" import threadingimport time counter = 0 # 无锁共享计数器lock = threading.Lock()counter_mutex = 0 # 加锁保护后的计数器 def add_without_lock(times): global counter for _ in range(times): v = counter # 读 time.sleep(0.000001) # 放大非原子窗口 counter = v + 1 # 写 def add_with_lock(times): global counter_mutex for _ in range(times): with lock: # 等价于 Rust 的 Mutex lock 守卫 counter_mutex += 1 def demo_race(): global counter counter = 0 threads = [threading.Thread(target=add_without_lock, args=(1000,)) for _ in range(2)] for t in threads: t.start() for t in threads: t.join() print("无锁结果:", counter, "(期望 2000,实际更小 => 数据竞争)") def demo_mutex(): global counter_mutex counter_mutex = 0 threads = [threading.Thread(target=add_with_lock, args=(1000,)) for _ in range(2)] for t in threads: t.start() for t in threads: t.join() print("Mutex 结果:", counter_mutex, "(期望 2000,正确!)") def demo_try_lock(): got = lock.acquire(blocking=False) # try_lock 语义 if got: print("try_lock: 拿到了锁,开始工作") lock.release() else: print("try_lock: 锁被占用,先干别的") lock_a = threading.Lock()lock_b = threading.Lock() def deadlock_part(idx): first = lock_a if idx == 0 else lock_b second = lock_b if idx == 0 else lock_a ok_first = first.acquire(timeout=0.3) if not ok_first: return time.sleep(0.02) ok_second = second.acquire(timeout=0.3) if not ok_second: print("线程", idx, "等锁超时,放弃等待(避免死锁卡死)") else: second.release() first.release() def demo_deadlock(): threads = [threading.Thread(target=deadlock_part, args=(i,)) for i in range(2)] for t in threads: t.start() for t in threads: t.join() print("死锁演示结束: 两个线程互等超时后退出") if __name__ == "__main__": demo_race() demo_mutex() demo_try_lock() demo_deadlock()
v = counter把共享计数器读到局部变量,读与写分离是竞争的前提time.sleep(0.000001)人为放大“读改写”之间的非原子窗口,让线程更容易被切换counter = v + 1写回结果,若中途被插队就会覆盖别人的更新,最终缺数with lock:等同于 Rust 的 lock 守卫:进入自动加锁,离开自动解锁got = lock.acquire(blocking=False)对应 Rust 的try_lock():拿不到锁立即返回 Falsefirst = lock_a if idx == 0 else lock_b两个线程按相反顺序取锁,制造死锁前提second.acquire(timeout=0.3)带超时获取,等价于“等不到就先让路”,避免永久死锁
常见误区
误区我一直以为 Rust 的所有权机制能在编译期解决所有并发问题,既然内存安全都保证了,那共享数据应该也不用加锁吧?
正解所有权解决的是“谁拥有内存、何时释放”的静态规则,而并发是“多个线程动态交错执行”的运行时问题。共享数据要支持并发修改,依然必须用 Mutex<T> 或 RwLock<T> 建立同步。Rust 真正做的是:把 C++ 里“你写错了编译器不管”变成“你写错了编译直接报错”,但锁依然是并发编程的必需品,不是可选项。
为何常见:初学者容易把“内存安全”和“线程安全”混为一谈,误以为一道防线万能;实际上二者是不同层次的问题。
误区既然 Send 和 Sync 只是标记,那我给自己的结构体写一句 `unsafe impl Send + Sync`,是不是就能随意在线程间共享数据了?
正解可以写,但这等于向编译器承诺“我用人格担保这个类型线程安全”。如果结构体内部藏着裸指针、Cell 或非线程安全的 FFI 句柄,这个承诺就是谎言,运行时会产生未定义行为(UB)。正确的姿势是让编译器自动推导,只有在你实现自定义底层类型、并能证明安全的少数场景,才用 unsafe impl。
为何常见:看到"unsafe"就以为能绕过限制,却不知道 unsafe 是把安全检查责任从编译器转移到了自己肩上。
误区读写锁支持多个读者并行,听起来就比互斥锁高级,那所有场景下都应该用 RwLock 吧?
正解RwLock 需要维护读者计数和等待队列,加锁开销本身比 Mutex 大。在写操作频繁或读写均衡的场景,RwLock 的优势被抵消,Mutex 反而更快。只有在读操作占绝对多数(读多写少)时,RwLock 的并行读能力才能换来净收益。选锁要看场景,不是看名字酷不酷。
为何常见:只看“读并行”的宣传,忽略了锁实现的额外开销与写竞争的成本。
误区try_lock 拿到锁失败,那我就用 while 循环不断重试,直到拿到为止,这样总不会错过机会吧?
正解立即重试等于忙等(busy waiting),会白白烧掉 CPU,还可能让持锁线程更难被调度、加剧竞争。正确的做法是退避后重试,或先去执行其他任务,稍后再回来尝试。如果预期等待时间极短,应使用专门的 spin lock,而不是拿 try_lock 硬转。
为何常见:把“不阻塞”误解为“疯狂尝试”,忽略了 try_lock 的设计初衷是让线程在拿不到锁时去做别的事。
误区既然 Arc 能让多个线程共享同一份数据,那就不需要 Mutex 了吧?直接用 Arc 包起来不就行了?
正解Arc 只负责“引用计数”,解决的是“这块数据归谁管、何时释放”;它不提供任何互斥机制,两个线程拿着同一个 Arc<T> 还是可以同时读改写,照样数据竞争。必须在里面套上 Mutex 或 RwLock:Arc<Mutex<T>> 才是“既共享又安全”的完整方案。
为何常见:混淆了“共享所有权”和“共享访问”两个概念,把计数工具误当成同步工具。
面试题
Q1. 数据竞争发生的三个必要条件是什么?
- A.多个线程共享内存且至少一个写且无同步← 正确答案
- B.多个线程各自操作私有变量且完全独立
- C.单个线程反复读写同一块内存数据
- D.多个线程同时读取同一份只读数据
答案:A。数据竞争需要三个条件同时成立:共享内存、至少一个写、无同步。B 是私有变量互不干扰,C 是单线程,D 全是读操作,都不满足竞争条件。
Q2. 在 Rust 中,Send trait 表示什么?
- A.类型可安全地在线程间转移所有权← 正确答案
- B.类型可被多个线程安全地只读共享
- C.类型可以跨进程进行序列化传递
- D.类型能够自动实现克隆复制功能
答案:A。Send 的含义就是“所有权能安全地跨线程搬家”,对应厨房里“连袋子递面粉”。B 描述的是 Sync 的场景;C 是序列化问题;D 是 Clone trait 的职责。
Q3. 关于 Send 与 Sync 之间的关系,正确的是?
- A.T 的只读引用可跨线程则 T 是 Sync← 正确答案
- B.T 能跨线程转移所有权则 T 是 Sync
- C.Sync 类型自动实现 Send 的约束
- D.Send 与 Sync 是等价的两个约束
答案:A。T: Sync 当且仅当 &T: Send,即“借阅证能跨线程发,说明被借的东西适合围观”。B 说的是 Send 的定义;C、D 混淆了 Send 与 Sync 的分工。
Q4. Mutex 的“中毒(poisoned)”状态指的是什么?
- A.持锁线程 panic 导致锁进入异常状态← 正确答案
- B.多个线程同时抢到了同一把锁
- C.锁被编译器自动优化成无锁操作
- D.锁内部的数据被无条件地丢弃
答案:A。持锁线程 panic 时,锁内数据可能处于半更新状态,Rust 将锁标记为 poisoned,后续 lock() 会返回 PoisonError 提醒你。B 在正确设计的锁里不可能发生;C 与编译优化无关;D 不是中毒语义。
Q5. RwLock 最适合以下哪种场景?
- A.读多写少时提供更高的并发吞吐← 正确答案
- B.写多读少时减少竞争等待
- C.所有并发场景均优于 Mutex
- D.只有纯只读数据时才值得使用
答案:A。RwLock 读锁可并行、写锁独占,天生服务“读多写少”。B 写多时应选 Mutex;C 过度绝对,读写均衡时 RwLock 开销更大;D 如果纯只读根本不需要锁。
Q6. Arc\<T> 的核心作用是什么?
- A.原子引用计数实现多线程共享所有权← 正确答案
- B.提供原子操作实现无锁数据读写
- C.替代 Mutex 提供互斥访问能力
- D.自动释放每个线程自己的栈内存
答案:A。Arc 就是“存货登记簿”,用原子计数让多个线程各持一份访问权,计数归零才释放数据。B 是原子类型的职责;C 是锁的职责;D 栈内存本就会自动释放。
Q7. 死锁的四个必要条件是?
- A.互斥、持有等待、不可抢占、循环等待← 正确答案
- B.互斥、竞争、饥饿、优先级反转策略
- C.共享、原子性、一致性、持久性
- D.饿死、冲撞、活锁、超时后的重试
答案:A。Coffman 四条件:互斥、持有并等待、不可抢占、循环等待,四个连环扣同时锁死就是死锁。B 混入饥饿、优先级反转;C 是数据库 ACID;D 是其他并发现象。
Q8. 以下哪些场景更适合用 Mutex 而不是 RwLock?
- A.读写频率接近且持锁区间很短← 正确答案
- B.读操作占绝大多数的配置读取
- C.写操作频繁且临界区较小← 正确答案
- D.只读循环遍历共享的超大数组
答案:AC。读写均衡或写频繁时,RwLock 的读者计数与等待队列开销不划算,Mutex 更简单高效。B 和 D 是典型的读多写少场景,RwLock 能带来更高的读并发。
Q9. 以下哪些策略能有效避免死锁?
- A.所有线程按相同顺序获取锁← 正确答案
- B.获取锁失败后立刻让路← 正确答案
- C.尽量缩短持锁的临界区← 正确答案
- D.把锁粒度调大到全部数据
答案:ABC。统一加锁顺序打破循环等待;try_lock 失败立刻让路避免死等;缩短持锁时间降低“持有并等待”的窗口。D 把锁变大反而降低并发度,且不解决循环等待问题。
Q10. 关于 Arc\<Mutex\<T>> 的组合,下列说法正确的是?
- A.Arc 负责共享所有权的引用计数← 正确答案
- B.Mutex 保证同一时刻互斥访问← 正确答案
- C.该组合实现安全的并发数据修改← 正确答案
- D.该组合创建后不可跨线程使用
答案:ABC。Arc 管“多线程各持一份权”,Mutex 管“同时只有一个人动手”,二者组合才能安全地并发修改共享数据。D 正好说反了——这个组合的目的就是让多个线程安全地一起使用它。