C++ 锁的底层原理详解:从原子操作到操作系统内核
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++ 锁的底层原理可以概括为以下几个层次:
- CPU 原子指令层:锁的根基是 CPU 提供的原子指令——CAS(
cmpxchg)、LL/SC(ldrex/strex)、原子交换(xchg)。这些指令保证“检查并设置”操作的原子性,是锁实现的最小单位。 - 自旋锁层:最基础的锁实现,完全在用户态运行。使用
test_and_set等原子操作在循环中等待锁释放。适合临界区极短的场景。std::atomic_flag提供了实现自旋锁所需的基础。 - futex/内核辅助层:现代互斥锁的实现核心。futex 的设计哲学是“能用户态解决就不进内核”——无竞争时一条原子指令完成,有竞争时才通过系统调用挂起线程。这是 Linux 上
std::mutex的基础。 - 标准库封装层:
std::mutex、std::shared_mutex、std::lock_guard等是对底层原语的安全封装,提供 RAII 支持和异常安全。
关键理解:
- 锁的实现始终依赖原子操作——没有原子操作就没有锁
- 快速路径是无竞争时的关键优化——一次原子 CAS 即可,无需系统调用
- 慢速路径通过操作系统内核挂起线程——涉及上下文切换,开销大
- 锁的性能不仅取决于实现,更取决于竞争程度——减少临界区大小、降低锁粒度是根本的优化方向
理解锁的底层原理,不仅是为了在面试中回答“锁是怎么实现的”,更是为了在性能优化时做出正确的决策——何时用自旋锁、何时减少锁粒度、何时接受系统调用的开销。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)