为了追求极限性能,cpu硬件搞出了独立缓存(L1/L2)和乱序执行,但也留下了“数据不同步”和“顺序错乱”的烂摊子。硬件用MESI协议和内存屏障指令来修补。编程语言为了抹平不同平台(xv6/arm)的差异,制定了内存模型契约,让程序员通过指定内存序(relaxed/acquire/release/sc)来指挥编译器和cpu自动插入屏障。而程序员利用这些契约和底层的硬件CAS指令,最终构建出了无锁算法以及互斥锁等同步原语,为多线程并发提供了正确同步的基础设施。

摘要:

因为有 L1/L2 独立缓存(导致多核数据不同步) ➜ 所以硬件设计了 MESI 协议(第 1 层)

即使数据同步了,因为 CPU 乱序和写缓冲(导致多变量间顺序错乱) ➜ 所以硬件提供了 内存屏障与原子汇编指令(第 2 层)

硬件指令太难用了且平台不统一 ➜ 所以高级语言制定了统一的 高级语言内存模型(第 3 层) 来对齐标准。

程序员利用语言内存模型 ➜ 组合出了面向变量的 CAS 和无锁数据结构(第 4 层)

为了防止无锁自旋榨干 CPU,同时为了管理宏观复杂的业务 ➜ 最终联合操作系统内核开发出了 互斥锁 Mutex(第 5 层)

第1层:L1/L2 独立缓存

这一层对于程序透明,用户无法直接操控

  • 核心痛点:多级独立缓存导致的数据不同步
  • 解决方法:缓存一致性协议,如MESI

MESI保证缓存一致性,而非复合操作的原子性。单字读写原子性通常由CPU架构保证。

第2层:内存屏障与原子汇编指令

  • 核心痛点:即使单个数据同步了,因为 CPU 存在乱序执行和写缓冲区延迟,导致了多变量之间的执行顺序错乱。

  • 解决方法:硬件内存屏障(Memory Barrier / Fence)原子指令

    • 四种经典屏障:LoadLoad, LoadStore, StoreStore, StoreLoad。
    • 硬件 CAS(Compare-And-Swap)(如 x86 的 LOCK CMPXCHG)。
  • 锁定机制:缓存锁(Cache Locking) vs 总线锁(Bus Locking)。

第3层:高级语言内存模型与编译器

  • 核心痛点:不同 CPU 架构(x86 和 ARM)的指令完全不统一,而且编译器会代码优化重排。

  • 解决方法:高级语言联合制定了统一的 高级语言内存模型契约

  • 硬件内存模型:强内存模型(x86)和弱内存模型(arm/riscv)

  • 软件内存模型:数据竞争和happens-before

    • 宽松relaxed:最弱,只保证操作的原子性,不保证顺序
    • release:写入缓存后会强制刷新本地缓存,保证release前的操作一定发生,但是不会全局同步到其他线程
    • acquire:读取时会使本地缓存无效再读取,保证之后的操作不会在acquire操作之前执行
    • 顺序一致性seq_cst:最强,每次写入缓存时会暂停并等待所有线程同步完成后再继续执行,每次读取会标记本地缓存无效后再读取,保证所有线程看到的操作的顺序一致

    注意:这只是本人抽象出来的模型,不代表具体的硬件是这样执行的

第4层:原子操作

4.1 语言级原子操作(Atomic Operations)

  • 承上启下:高级语言(C++11/Rust/Java)将第 2 层的硬件原子指令(如 LOCK CMPXCHG)与第 3 层的内存顺序(Memory Order)绑定,封装为面向对象的原子类型(如 std::atomic)。
  • 原子核心操作(软件 CAS)
    • compare_exchange_strong:绝对可靠的比较并交换,底层对应硬核锁机制。
    • compare_exchange_weak:允许伪失败(Spurious Failure)的 CAS。在 ARM 等弱内存模型(LL/SC 架构)上,它的循环效率比 strong 更高。
  • 特性:所有原子操作均在用户态(User Space)完成,不发生上下文切换(Context Switch),没有内核开销。

第 5 层:基于原子操作构建同步机制

5.1 Mutex(互斥锁)

  • 核心痛点:第 4 层的自旋锁和无锁结构在竞争极其激烈时,会导致大量线程在用户态疯狂重试、空转,造成严重的 CPU 资源浪费。
  • 解决方法(混合锁机制 Fast-Path / Slow-Path)
    • Fast-Path(无竞争,用户态解决):线程调用 mutex.lock() 时,内部先尝试一次轻量级的 CAS。如果此时没有竞争,直接抢锁成功,整个过程完全不惊动操作系统内核
    • Slow-Path(有竞争,内核态挂起):如果 CAS 失败,线程不再自旋。它会通过系统调用(System Call,如 Linux 的 futex)陷入操作系统内核。内核的调度器将该线程的状态改为“阻塞/休眠”,挂入该锁的等待队列,并触发上下文切换,把 CPU 让给其他有用的线程。
    • 唤醒:当锁持有者调用 unlock() 时,再次陷入内核,由内核将等待队列里的休眠线程唤醒,重新放入 CPU 就绪队列。

5.2 RWLock(读写锁)

  • 演进逻辑:在“读多写少”的业务场景下,Mutex 无论是读是写一律互斥,效率太低。
  • 内部机制:内部通常维护两个控制变量:一个记录当前活跃的读者数量(Counter),一个记录是否有写者在等待(Flag)。
  • 行为特征
    • 读锁(Shared Lock):利用原子操作自增读者计数(Counter),允许多个线程同时获取读锁。
    • 写锁(Exclusive Lock):表现退化为传统 Mutex,当计数器不为 0 时必须陷入内核阻塞休眠。
  • 策略抉择:需要处理“读者优先”还是“写者优先”的饥饿问题(防止写者被源源不断的读者无限期卡死)。

5.3 Spinlock(自旋锁)

  • 演进逻辑:当我们需要保护一段极短的临界区,又不想让线程去睡觉时,最直接的方法就是让 CPU 用原子操作进行“死循环硬扛”。
  • 构造原理:利用一个原子的布尔标记。
    • 加锁(Lock):使用 relaxedacquire 原子操作,在一个死循环里不断执行 CAS,尝试将标记从 false 改为 true。如果失败,CPU 就原地空转。
    • 解锁(Unlock):使用 release 内存序,直接将标记写回 false。由于使用了 release,可以确保临界区内所有的修改在解锁这一刻全部刷新,对下一个抢到锁的线程可见。
  • 优缺点:响应速度极快(纳秒级),但高并发竞争时会榨干 CPU 算力

5.4 Lock-Free(无锁)数据结构

  • 演进逻辑:为了彻底消除“锁”带来的线程挂起和死锁问题,程序员利用 CAS + 循环重试(Retry Loop) 来构建复杂的数据结构。
  • 核心代表:无锁栈(Treiber Stack)、无锁队列(Michael-Scott Queue)、原子自增计数器。
  • 运作机制(以无锁入队为例)
    1. 读取当前队列的尾节点指针 tail
    2. 在用户态准备好新节点。
    3. 使用 CAS(&tail->next, nullptr, new_node) 尝试挂载新节点。
    4. 如果中途被别的线程捷足先登(CAS 失败),说明有竞争,放弃当前操作,重新读取最新的 tail 并循环重试,直到成功。
  • 无锁陷阱ABA 问题(一个值从 A 变成 B 又变回 A,CAS 无法察觉)。解决方法通常是引入版本号或使用内存管理黑科技(如 Epoch-based Reclamation, Hazard Pointers)。

5.5 Semaphore(信号量)

  • 演进逻辑:Mutex 只能保护稀缺度为 1 的资源(非黑即白)。如果某种资源的数量是 $N$(如数据库连接池、限流器的并发上限),就需要用到信号量。
  • 内部机制:内部维护一个原子的资源计数器(Permits)和一个内核线程等待队列。
  • 行为特征
    • P 操作(Acquire/Wait):尝试原子地将计数器减 1。如果计数器 $\ge 0$,直接放行;如果计数器 $< 0$,说明资源枯竭,线程通过系统调用进入内核休眠排队。
    • V 操作(Release/Signal):原子地将计数器加 1。如果加 1 后计数器仍 $\le 0$,说明队列里有嗷嗷待哺的休眠线程,内核会负责唤醒队列头部的线程。

应用层:基于同步机制构建并发程序

当熟练掌握了 1 到 5 层的全部原理后,在实际编写上层并发程序(如高性能 Web 服务器、分布式存储引擎)时,思考逻辑将上升到宏观的设计模式:

  1. 临界区最小化原则:无论使用有锁还是无锁,永远让锁/原子操作保护的代码块尽可能短,避免在临界区内做耗时的 I/O 或大内存分配。
  2. 避免死锁(Deadlock 防范):在使用第 5 层的 Mutex/RWLock 时,若涉及多把锁,必须严格规定加锁顺序(锁分级制度),或者使用破坏死锁必要条件的尝试加锁机制(如 try_lock)。
  3. 线程间协同协作:结合第 5 层的条件变量(Condition Variable)与 Mutex 共同构建经典的“生产者-消费者”模型。利用条件变量让线程在“等待某个条件成立”时挂起,避免用 while 循环去轮询原子变量。
  4. 架构选择偏好
    • 对于高吞吐、极致响应、竞争不激烈的底层基础库(如内存分配器 jemalloc, 无锁网络框架):优先采用 4层无锁算法与自旋锁
    • 对于业务复杂、临界区长、竞争激烈的上层应用(如数据库事务管理、文件读写流):优先采用 5层互斥锁、读写锁与信号量,用内核调度换取系统的平稳与高资源利用率。