C++ 锁的底层原理详解:从原子操作到操作系统内核



一、引言:锁是如何实现的


在高层次上,锁(std::mutex)提供了互斥访问的保证:同一时间只有一个线程能进入临界区。但锁本身是如何实现的?它如何保证“检查锁状态”和“设置锁状态”这两个操作是原子的?当锁不可用时,等待的线程是如何被阻塞的?


理解锁的底层原理,不仅有助于选择合适的同步原语,还能帮助理解性能瓶颈的来源——什么时候锁会导致系统调用?自旋锁和互斥锁在底层有什么不同?本文将逐层深入,从 CPU 原子指令到操作系统调度。



二、锁的实现层次概览


图表代码下载全屏

三、核心基础:CPU 原子指令



3.1 为什么需要原子指令


锁的实现面临一个根本问题:检查和设置锁状态是两个步骤,如果这两个步骤不原子,两个线程可能同时通过检查,都认为自己获得了锁。


cpp复制下载

// ❌ 软件层面的锁实现(错误示范)
struct BadLock {
    bool locked = false;
    
    void lock() {
        // 步骤1:检查锁状态
        while (locked) { }
        // 步骤2:设置锁状态
        locked = true;
        // 这两个步骤不是原子的!
        // 线程A检查到 false → 线程B也检查到 false → 两个都进入临界区!
    }
};



3.2 关键原子指令



| 指令 | 平台 | 功能 |

| --- | --- | --- |

| cmpxchg (CAS) | x86/x64 | 比较并交换:if [dst]==eax then [dst]=ebx else eax=[dst] |

| xchg | x86/x64 | 原子交换:swap(reg, mem) |

| lock inc/dec | x86/x64 | 原子递增/递减 |

| ldrex/strex (LL/SC) | ARM | 加载链接/条件存储 |

| mfence/lfence/sfence | x86/x64 | 内存屏障 |



3.3 CAS 指令详解


asm复制下载

; x86-64 汇编:cmpxchg 指令
; 伪代码:
; if (RAX == [memory_destination])
;     [memory_destination] = new_value
;     ZF = 1  (成功)
; else
;     RAX = [memory_destination]
;     ZF = 0  (失败)

; 实际使用:
mov     eax, expected_value    ; 期望值
mov     ebx, desired_value     ; 新值
lock cmpxchg [memory], ebx    ; 原子地:if [mem]==eax then [mem]=ebx


图表代码下载全屏

四、自旋锁:最基础的锁实现



4.1 使用 atomic_flag 实现自旋锁


自旋锁是最底层的锁实现,完全在用户态运行,不涉及系统调用。它使用原子操作的循环等待。


cpp复制下载

#include <atomic>
#include <thread>

class Spinlock {
    std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
    
public:
    void lock() {
        // test_and_set:原子地设置 flag_ 为 true,返回旧值
        // 如果旧值是 true(已被锁),则循环等待
        while (flag_.test_and_set(std::memory_order_acquire)) {
            // 自旋等待:CPU 在这里忙等待
            // 在 x86 上,可以在循环中加入 PAUSE 指令降低功耗
            #if defined(__x86_64__) || defined(__i386__)
            __builtin_ia32_pause();  // 提示 CPU 这是自旋循环
            #endif
        }
    }
    
    void unlock() {
        flag_.clear(std::memory_order_release);
    }
};

// x86-64 上 test_and_set 的底层汇编:
// lock bts [flag_], 0     ; 原子位测试并设置
// jc spin_loop             ; 如果进位标志=1(已被锁),继续循环



4.2 自旋锁的工作流程


图表代码下载全屏

4.3 自旋锁的优缺点



| 方面 | 自旋锁 |

| --- | --- |

| 系统调用 | 无 |

| 阻塞行为 | 忙等待,消耗 CPU |

| 适用场景 | 临界区极短(几微秒) |

| 不适用场景 | 临界区长,或可能发生 I/O |

| 实现复杂度 | 低 |



五、互斥锁:操作系统参与的睡眠锁



5.1 Linux futex 机制


std::mutex 在 Linux 上通常基于 futex(Fast Userspace Mutex) 实现。futex 的设计思想是:尽量在用户态解决问题,只在必要时才进入内核


cpp复制下载

// futex 的简化工作流程
// futex 系统调用:
// syscall(SYS_futex, addr, op, val, timeout, addr2, val3);

// 核心操作:
// FUTEX_WAIT: 如果 *addr == val,则挂起当前线程(进入内核等待)
// FUTEX_WAKE: 唤醒最多 val 个在 addr 上等待的线程



5.2 基于 futex 的 mutex 简化实现


cpp复制下载

#include <atomic>
#include <linux/futex.h>
#include <sys/syscall.h>
#include <unistd.h>

class FutexMutex {
    // 0: 未锁定
    // 1: 锁定,无等待者
    // 2: 锁定,有等待者
    std::atomic<int> state_{0};
    
    void futexWait(int expected) {
        syscall(SYS_futex, &state_, FUTEX_WAIT_PRIVATE, expected, 
                nullptr, nullptr, 0);
    }
    
    void futexWake(int count) {
        syscall(SYS_futex, &state_, FUTEX_WAKE_PRIVATE, count, 
                nullptr, nullptr, 0);
    }
    
public:
    void lock() {
        // 快速路径:尝试用原子操作获取锁(无系统调用)
        int expected = 0;
        if (state_.compare_exchange_strong(expected, 1, 
                std::memory_order_acquire)) {
            return;  // 成功获取锁!
        }
        
        // 慢速路径:锁已被持有
        // 如果锁已被持有但没有等待者,标记为有等待者
        if (expected != 2) {
            expected = state_.exchange(2, std::memory_order_acquire);
        }
        
        // 进入内核等待
        while (expected != 0) {
            futexWait(2);  // 挂起当前线程
            expected = state_.exchange(2, std::memory_order_acquire);
        }
    }
    
    void unlock() {
        // 如果 state 从 1 变为 0(无等待者),完成
        if (state_.fetch_sub(1, std::memory_order_release) == 1) {
            return;
        }
        
        // 有等待者:设 state 为 0,并唤醒一个等待线程
        state_.store(0, std::memory_order_release);
        futexWake(1);  // 唤醒一个等待的线程
    }
};



5.3 futex 的工作流程


图表代码下载全屏

5.4 快速路径 vs 慢速路径



| 路径 | 操作 | 开销 |

| --- | --- | --- |

| 快速路径 | 无竞争时,一次原子 CAS 即可获取锁 | ~10-20 个 CPU 周期 |

| 慢速路径 | 有竞争时,需要 futex 系统调用挂起线程 | ~数千个 CPU 周期(上下文切换) |



六、不同平台的实现



6.1 Linux:futex + atomic


cpp复制下载

// Linux 上 std::mutex 的核心结构(简化)
struct __mutex_base {
    std::__atomic_futex_unsigned data_;  // 0=unlocked, 1=locked, 2=locked+waiters
};
// 基于 futex 系统调用实现



6.2 Windows:SRWLock + atomic


cpp复制下载

// Windows 上 std::mutex 通常基于 SRWLock
// SRWLock (Slim Reader/Writer Lock) 是轻量级锁
// 内部使用原子操作 + WaitOnAddress/WakeByAddress
struct SRWLock {
    void* ptr;  // 低位用于锁状态,高位用于等待队列
};



6.3 macOS/iOS:os_unfair_lock + atomic


cpp复制下载

// macOS 上使用 os_unfair_lock
// 基于 Mach 内核的 ulock 机制
typedef struct {
    uint32_t os_unfair_lock_opaque;
} os_unfair_lock;



七、自旋锁 vs 互斥锁的对比


图表代码下载全屏

| 维度 | 自旋锁 | 互斥锁 |

| --- | --- | --- |

| 等待方式 | CPU 忙等待 | 线程挂起 |

| CPU 消耗 | 高(自旋期间) | 低(被挂起时) |

| 上下文切换 | 无 | 有(等待/唤醒) |

| 临界区推荐时长 | < 几微秒 | 任意长度 |

| 适用场景 | 极短临界区、中断上下文 | 通用场景 |



八、锁的性能层次


text复制下载

获取无竞争的锁
  → 快速路径:1 次原子 CAS 操作(~10 CPU 周期)
  → 极快,完全在用户态

获取有竞争的锁(自旋锁)
  → CPU 空转 ~数百个周期
  → 快,但浪费 CPU

获取有竞争的锁(互斥锁)
  → futex 系统调用(~数百-数千周期)
  → 线程上下文切换(~数千-数万周期)
  → 较慢,但不浪费 CPU



九、总结


C++ 锁的底层原理可以概括为以下几个层次:



  1. CPU 原子指令层:锁的根基是 CPU 提供的原子指令——CAS(cmpxchg)、LL/SC(ldrex/strex)、原子交换(xchg)。这些指令保证“检查并设置”操作的原子性,是锁实现的最小单位。
  2. 自旋锁层:最基础的锁实现,完全在用户态运行。使用 test_and_set 等原子操作在循环中等待锁释放。适合临界区极短的场景。std::atomic_flag 提供了实现自旋锁所需的基础。
  3. futex/内核辅助层:现代互斥锁的实现核心。futex 的设计哲学是“能用户态解决就不进内核”——无竞争时一条原子指令完成,有竞争时才通过系统调用挂起线程。这是 Linux 上 std::mutex 的基础。
  4. 标准库封装层std::mutexstd::shared_mutexstd::lock_guard 等是对底层原语的安全封装,提供 RAII 支持和异常安全。


关键理解



  • 锁的实现始终依赖原子操作——没有原子操作就没有锁
  • 快速路径是无竞争时的关键优化——一次原子 CAS 即可,无需系统调用
  • 慢速路径通过操作系统内核挂起线程——涉及上下文切换,开销大
  • 锁的性能不仅取决于实现,更取决于竞争程度——减少临界区大小、降低锁粒度是根本的优化方向


理解锁的底层原理,不仅是为了在面试中回答“锁是怎么实现的”,更是为了在性能优化时做出正确的决策——何时用自旋锁、何时减少锁粒度、何时接受系统调用的开销。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐