① 钩子lock 前缀到底锁住了什么lock前缀到底锁住了什么原子自增fetch_add一条lock xadd指令CAS 一条lock cmpxchg指令自旋锁 一条xchg 循环而std::mutex却是调进操作系统线程库的函数调用。这些锁的真相在汇编里一目了然。这一集我们拆四种锁原子 RMW、CAS、mutex、自旋锁看它们各自的机器码形态与成本。② 源码 vs 汇编对照__attribute__((noinline))intinc_atomic(std::atomicintc){returnc.fetch_add(1);}__attribute__((noinline))intinc_relaxed(std::atomicintc){returnc.fetch_add(1,std::memory_order_relaxed);}__attribute__((noinline))boolcas(std::atomicintc,intexpected,intdesired){returnc.compare_exchange_strong(expected,desired);}__attribute__((noinline))intinc_mutex(intc,std::mutexm){std::lock_guardstd::mutexlk(m);returnc;}classSpinLock{std::atomicboolflag{false};public:__attribute__((noinline))voidlock(){while(flag.exchange(true,std::memory_order_acquire)){}}__attribute__((noinline))voidunlock(){flag.store(false,std::memory_order_release);}};原子自增fetch_add→lock xaddinc_atomic(std::atomicint): movl $1, %eax lock xaddl %eax, (%rcx) ; 读旧值、加 1、写回 —— 三步合一且加锁 ret反直觉点memory_order_relaxed的 fetch_add 和它一模一样inc_relaxed(std::atomicint): movl $1, %eax lock xaddl %eax, (%rcx) ; 完全相同的指令 retCAS →lock cmpxchgcas(std::atomicint, int, int): movl %edx, %eax lock cmpxchgl %r8d, (%rcx) ; 比较并交换一条原子指令 sete %al ; 成功标志 retstd::mutex→ 调进系统线程库inc_mutex(int, std::mutex): call pthread_mutex_lock testl %eax, %eax jne .L7 ; 失败 → 抛 std::system_error movl (%rdi), %eax leal 1(%rax), %ebx movl %ebx, (%rdi) call pthread_mutex_unlock ... .L7: call __throw_system_error手写自旋锁xchg 转圈SpinLock::lock(): .L9: movl $1, %eax xchgb (%rcx), %al ; 原子交换读到旧值并置 1xchg 在 x86 上隐式加锁 testb %al, %al jne .L9 ; 旧值是 1 → 没抢到继续转圈 ret SpinLock::unlock(): movb $0, (%rcx) ; release 的普通 store ret环境备注本集在 x86-64 Windows MinGW g 15.2.0winpthreads实测std::mutex落到pthread_mutex_*。ARM 上原子指令形态不同ldxr/stxr配对 CAS 循环lock前缀是 x86 专属。③ 为什么这么设计原子操作 硬件提供的读-改-写原子指令。x86 上以lock前缀实现它会锁住内存总线/缓存行保证读旧值 → 改 → 写回三步不可分割。fetch_add→lock xaddcompare_exchange→lock cmpxchgexchange→xchgx86 上xchg隐式加锁。为什么 relaxed 也省不掉 lockx86 没有更便宜的原子 RMW——任何需要原子读-改-写的操作都必须加锁。所以memory_order在 x86 上对 RMW 指令几乎没影响它真正影响的是非 RMW 的 load/store 要不要插 fence这在 ARM 上才体现为不同指令。mutex不是原子std::mutex是操作系统线程库的实现这里落到pthread_mutex_*它内部有锁但对外是可能把线程挂起、让出 CPU、等被唤醒的阻塞原语——比自旋重得多但不会忙等。自旋锁原理用exchange(true, acquire)原子地把标志置 1 并拿到旧值。旧值是 0 锁是空闲的抢到了是 1 已被别人持有继续转圈。释放只是store(false, release)——release 语义保证锁保护的数据修改在解锁前对其他线程可见。④ 深入一x86 的原子是怎么实现的lock前缀在硬件层面做了两件事锁总线/缓存行确保读-改-写期间没有其他核心能修改同一内存位置保证可见性操作结果对所有核心可见配合缓存一致性协议。三种常见原子指令操作指令原子加/减lock xadd/lock sub或lock addCASlock cmpxchg交换xchgx86 上隐式 lockARM 不同ARM 没有lock前缀用ldxr/stxr独占加载/存储配对实现原子 RMW——失败就重试循环。所以同样一行 Cx86 是 1 条指令、ARM 是加载-尝试存储-失败重试的小循环。理解这点就理解原子操作跨平台指令不同。⑤ 深入二为什么 mutex 比原子重得多对比本集汇编原子 RMW1 条指令纳秒级不阻塞std::mutexpthread_mutex_lock是一次系统调用/库调用——可能把线程挂起睡眠、切换上下文、等唤醒。微秒级甚至更久但不忙等 CPU。选型计数器/标志位/简单 RMW → 原子快、无阻塞临界区多语句、长操作、要持锁 → mutex阻塞式不烧 CPU临界区极短 多核 可容忍忙等 → 自旋锁避免系统调用但空转。一个关键认识std::mutex内部也可能先自旋再睡眠自适应但对外语义是可能阻塞——所以它不适合每纳秒都抢锁的场景。⑥ 常见误区误区 1“memory_order_relaxed更快”在 x86 上对 RMW没有差别都是lock xadd。差别在 ARM/非 RMW 的 load/store。别在 x86 上幻想 relaxed 白赚性能。误区 2“原子操作绝对免费”lock前缀 缓存一致性有真实成本高竞争多核下 RMW 是热点。误区 3“mutex 就是原子”mutex 是阻塞原语可能睡眠/切换原子是非阻塞指令。用途完全不同。误区 4“自旋锁比 mutex 一定快”临界区稍长/单核/高竞争时自旋锁空转烧 CPU反而更差。自旋只适合极短临界区 多核。误区 5“原子变量就是线程安全的数据”原子只保证单个原子操作不可分割多个原子操作组成的逻辑仍可能被其他线程穿插需要更强的同步E17 展开。误区 6“lock前缀会锁住整个内存总线”现代 x86 的lock前缀主要锁缓存行缓存一致性协议不是整个总线——所以原子操作没有你想象的那么慢但高竞争多核下仍会争抢缓存行。误区 7“std::atomic只适合整数”任何 trivially copyable 类型都能原子化std::atomicdouble、std::atomicMyStruct但大结构体可能退化为内部加锁is_lock_free()为 false。小 POD 才真正无锁。误区 8“compare_exchange_weak比 strong 安全”weak 允许虚假失败硬件偶发用它必须配失败重试循环strong 保证不虚假失败。weak 不是更安全是更适合循环重试场景更快。⑦ 实战启示计数器/标志位用std::atomic一条指令临界区用std::mutex会阻塞不忙等适合持锁时间长的场景。自旋锁适合临界区极短 多核但持锁期间别做阻塞/IO/系统调用——其他核会空转烧 CPU。别轻易手写无锁正确性门槛极高错误比性能损失贵得多。先证明锁是瓶颈再考虑无锁/自旋。理解lock前缀的代价RMW 会触及缓存一致性协议多核高竞争时是热点可能比预期的慢。用 RAII 拿锁lock_guard/unique_lock异常/提前返回时自动释放避免死锁E07 的 RAII 精神。⑧ 扩展专题一CAS 循环——读-算-再试的原子模式compare_exchange_strong常用来实现读旧值 → 基于旧值计算 → 尝试更新的循环std::atomicinta{0};intcura.load();while(!a.compare_exchange_weak(cur,cur1)){// cur 已被更新为最新值weak 可能虚假失败重试}汇编形态lock cmpxchg失败后跳回重读重算循环weak vs strongweak 可能虚假失败硬件层面偶发用循环时更合适快strong 保证不虚假失败但更慢这是无锁更新的标准模式不锁整段用单条原子指令 重试。代价高竞争下 CAS 循环可能反复失败重试活锁风险。所以极低竞争用 CAS、高竞争用锁/分区是工程经验。⑨ 扩展专题二自旋锁的正确姿势与已获取锁后重入问题自旋锁的工程坑重入reentrant问题自旋锁不可重入——同一线程已持有再lock()会死锁永远转圈。std::recursive_mutex可重入但自旋锁一般不自带。持锁时长持锁期间做 IO/系统调用/长计算 其他核空转灾难。临界区必须极短。内存序exchange(acquire)/store(release)的配对保证锁保护的数据有序可见——写错顺序就成 bugE17 展开。yield/退避高竞争时纯xchg转圈太激进可用std::this_thread::yield()或退避降低缓存压力。一句话自旋锁是极短临界区专用用错场景比 mutex 更糟。标准库的std::atomic_flag 自旋是官方自旋锁的基础。⑩ 扩展 FAQQstd::atomicbool和std::atomic_flag区别Aatomic_flag最轻只有 test/set/clear保证无锁atomicbool更通用支持 load/store/exchange。无锁标志用atomic_flag。Qstd::atomic一定无锁吗A不一定。std::atomicT对可平凡复制的类型通常无锁is_lock_free()可查但大结构体可能退化为内部加锁实现。查is_always_lock_free确认。Qcompare_exchange的 expected 为什么是引用A失败时 expected 会被更新为当前实际值——这样调用方不用重读。这是循环重试能省一次 load 的设计。Qstd::mutex和std::recursive_mutex区别Arecursive_mutex允许同一线程重复加锁重入普通 mutex 重入会死锁。但重入常是设计问题的信号锁粒度不清。Q锁和原子能混用吗A能原子做标志、锁做临界区但别混用保护同一数据的两套机制——容易漏。统一一种同步手段更清晰。⑪ 扩展实验数指令g -O2 -S E16_atomic.cpp数原子 RMW1 条、mutex2 次 pthread 调用、自旋xchg 循环的指令数。relaxed vs seq_cst对比fetch_add两种 memory_order 的反汇编确认 x86 上相同。CAS 循环写无锁累加的 CAS 循环反汇编看失败重试路径。mutex 阻塞两线程争一把锁持锁线程sleep观察另一线程是否阻塞不忙等。自旋锁竞争多线程抢自旋锁做累加 vs mutex 做累加计时对比自旋在极短临界区可能更快稍长则更慢。⑬ 扩展专题三为什么说atomic 是协作的mutex 是强制/协作的混合一个常见误解是原子操作能保护共享数据——其实原子只对使用原子变量的代码生效线程 A 用atomic写、线程 B 用atomic读同步生效线程 A 用atomic写、线程 B 用普通变量读数据竞争UB——原子帮不了普通访问mutex 则对被锁保护的临界区强制串行——只要所有访问都持锁。所以用原子所有访问该变量的代码都必须用原子或普通访问也要有同步否则白用用锁所有访问该数据的代码都必须持锁漏一处就是竞争。工程结论小变量、单点更新用原子复合数据、多语句临界区用锁最关键的是统一访问路径——别一半原子一半普通。⑭ 扩展专题四锁粒度与性能的关系锁粒度决定并发的窗口大粒度粗锁一个全局锁保护所有数据——简单但并发度低串行化小粒度细锁按桶/行/条目加锁——并发度高但锁多、开销大、易死锁读写锁shared_mutex读读并发、读写互斥——适合读多写少。汇编层面每次lock操作都有缓存一致性成本锁越多原子开销越多。粒度是正确性 vs 并发度 vs 锁开销的三角平衡。经验先粗后细用 profiler 找到瓶颈再细化。⑮ 扩展 FAQ第二轮Qstd::atomicT的load()/store()也带 lock 吗A不。纯load/store是普通movx86 上天然有原子性需要时插 fenceE17 展开。带lock的只有 RMWxadd/cmpxchg/exchange。Q什么是无锁数据结构A不用锁mutex只用原子指令实现并发数据结构栈/队列/哈希。正确性极难竞争时可能用 CAS 重试。别轻易写。Qstd::atomic能用于浮点吗Astd::atomicdouble合法按位原子。但浮点比较更新的 CAS 循环语义要小心NaN 等。Q为什么volatile不能替代原子Avolatile只禁止编译器优化访问E17 会看到三条 mov不保证原子性/可见性/顺序。并发同步用atomic不是volatile。Q死锁怎么避免A锁顺序一致、RAIIlock_guard 自动释放、避免持锁调用外部未知代码、必要时用std::lock一次锁多个。这些都是锁正确性的工程规矩。Q为什么lock_guard是首选ARAII 保证异常/提前返回时解锁E07 讲过析构必调用。手写lock()/unlock()一旦漏掉 return/throw 就死锁。Qstd::atomic能保证读到的都是最新值吗A原子保证读到的要么是某次完整写的结果且配合内存序保证可见性顺序——但不保证一定是最新可能读到稍旧值除非有同步点。这是内存模型的微妙处E17 展开。⑯ 扩展实验第二轮load/store 反汇编atomicint的load/storevs 普通读写确认 x86 上都是普通 mov。读写锁shared_mutex读读并发 vs 写写反汇编/计时对比E17 的 shared_mutex 是 C17。锁粒度实验全局锁 vs 每桶锁哈希表多线程插入计时对比并发度差异。死锁演示两个线程按相反顺序锁两把锁运行触发死锁用std::lock修复。CAS 循环吞吐高竞争下 CAS 循环累加 vs 原子 xadd 累加计时对比重试开销。⑱ 扩展专题五原子指令 vs 锁——完整的成本对比表维度原子 RMWstd::mutex自旋锁汇编形态1 条lock xadd等系统调用/库调用xchg 转圈循环是否阻塞否是可睡眠/切换否忙等单次成本纳秒级微秒级含系统调用纳秒级但持续烧 CPU高竞争缓存行争抢变慢排队/唤醒吞吐可控空转烧 CPU更糟适用单点更新/计数临界区/多语句极短临界区多核结论没有万能同步。原子最快但只能做单点mutex 最通用但最重自旋锁介于中间但对场景挑剔。先想临界区多长、竞争多高再选工具。⑲ 扩展专题六atomic_flag 与真正的自旋锁模板标准库官方自旋锁是std::atomic_flagclassspinlock{std::atomic_flag flagATOMIC_FLAG_INIT;public:voidlock(){while(flag.test_and_set(std::memory_order_acquire)){std::this_thread::yield();// 高竞争时让出减少空转}}voidunlock(){flag.clear(std::memory_order_release);}};test_and_set 原子的置 1 并返回旧值x86 上xchg/btsclear 原子的置 0加 yield竞争激烈时让出时间片避免所有核空转刷缓存行——这是自适应退避的简单版。为什么标准库还要给std::mutexatomic_flag自旋锁没有睡眠/唤醒长临界区会饿死其他线程mutex 由操作系统管理队列公平且省 CPU。自旋 极短临界区专用mutex 通用。⑳ 扩展 FAQ第三轮Qstd::atomicint的operator和fetch_add一样吗A一样c就是fetch_add(1)的语法糖反汇编都是lock xadd。Q为什么 C 要把memory_order暴露给程序员A让高端用户能在正确性与性能间选最弱序ARM 上 relaxed 少 fence、更快。x86 上差异小但跨平台代码需要它。Qstd::mutex加锁失败会怎样Alock()会阻塞等待不返回try_lock()立即返回 false。锁失败抛异常的是lock()遇到系统错误时抛std::system_error本集.L7路径。Q原子操作一定比锁快吗A单次 RMW 是纳秒 vs 微秒但多步逻辑用原子拼可能比一把锁包住更复杂更慢。正确性优先再谈快慢。Q什么时候用std::atomic CAS 而不是 mutexA临界区就是单条 RMW 或极短 CAS 循环时。要保护的是复合状态/多字段时mutex 更简单可靠。㉑ 扩展实验第三轮atomic_flag 自旋用 ⑲ 模板实现自旋锁-O2 -S看test_and_set的原子指令与 yield 调用。竞争对比1/2/4/8 线程抢同一atomicint自增 100 万次观察吞吐随核数变化缓存行争抢。锁 vs 原子吞吐同样的累加分别用 mutex 临界区、原子 RMW、CAS 循环测吞吐对比。try_lock 实验try_lock失败立即返回 false验证非阻塞语义。错误路径人为让pthread_mutex_lock失败难或看__throw_system_error路径的汇编理解错误处理。㉓ 扩展专题七从锁到无锁的决策流程什么时候值得从锁换成无锁给一个务实流程先证明锁是瓶颈profiler 显示锁竞争占了可观比例评估临界区很短几条指令才可能无锁长临界区无锁化收益有限且极难选无锁原语计数器 →fetch_add单次发布 → acquire/release复杂结构 → 无锁队列/哈希有成熟库就借别自己造验证正确性TSan 压力测试 模型检查如果可负担保留回退高竞争时无锁可能比锁更差CAS 重试风暴必要时混用。一句话无锁是用内存序换取吞吐的高级优化门槛在正确性。先锁测出瓶颈再谨慎无锁——大多数代码用不到无锁。㉔ 扩展专题八缓存行与伪共享——锁之外的性能杀手多线程各自写不同的变量也可能互相拖慢——如果它们落在同一缓存行structCounter{longlonga;longlongb;};// a、b 相邻同一缓存行std::atomiclonglonga,b;// 线程1写a、线程2写b// 线程1: a.fetch_add(1) → 每次写都会使线程2的 b 缓存行失效// 线程2: b.fetch_add(1) → 互相踢对方的缓存行 → 伪共享缓存一致性协议按**缓存行64B**粒度同步a、b 同处一行谁写都让对方的行失效结果是无共享变量却互相拖慢几个数量级解法alignas(64)把每个计数器放到独立缓存行E06 讲过 alignas。启示并发性能不只是锁还是无锁还有数据布局——让不同线程写的数据尽量不在同一缓存行或至少知道它们会共享。㉕ 扩展 FAQ第四轮Qstd::atomic的wait/notifyC20是什么A原子等待/通知原语wait阻塞直到值变化、notify唤醒。比自旋省 CPU睡眠比 mutex 更针对单值等待。Q自旋锁的yield和pause是什么Ayield让出时间片进就绪队列pausex86提示 CPU我在自旋降低功耗/避免流水线风暴。高竞争自旋常两者都用。Qstd::mutex的try_lock会忙等吗A不会立即返回 false非阻塞。但lock()内部实现可能先自旋一小会儿再睡眠自适应锁。Q锁住的数据越大越慢吗A持锁时间越长其他线程等越久吞吐下降。所以锁粒度持锁范围是并发性能的核心杠杆之一。Q什么时候用读写锁A读多写少配置、缓存。std::shared_mutex允许多读并发、单写独占。写少时吞吐明显提升但锁本身更重。㉖ 扩展实验第四轮伪共享复现两线程各自增相邻long longvsalignas(64)分离计时对比差距可到数倍。wait/notifyC20 的atomic::wait/notify实现生产者-消费者反汇编对比与自旋/mutex 的差异。读写锁吞吐读多写少场景shared_mutexvsmutex计时对比。自适应锁观察持锁极短 vs 较长时 mutex 行为反汇编/性能体会先自旋再睡眠。锁粒度实验全局锁 vs 每桶锁的哈希表多线程插入吞吐对比。㉗ 悬念原子指令保证一次读改写不可分割可 CPU 会乱序执行——你写的操作顺序另一个线程看到的不一定是那个顺序。lock前缀之外内存模型和 fence 到底管什么