futex本身不是锁,而是地址绑定的等待/唤醒原语,无法单次调用完成“检查值+条件等待”的原子组合,必须配合用户态原子操作(如CAS)和严格FUTEX_WAIT/FUTEX_WAKE协议才能实现正确Mutex。

为什么不能直接用 futex 实现一个“完全正确”的用户态 Mutex
因为 futex 本身不是锁,它只是一个**等待/唤醒原语**,且不保证原子性组合。你无法靠单次 futex 调用完成“检查值 + 条件等待”这个关键操作——中间存在竞态窗口。Linux 内核明确要求:必须配合用户态的原子操作(如 __atomic_compare_exchange_n)一起使用,且需严格遵循 FUTEX_WAIT/FUTEX_WAKE 协议。
常见错误是写成这样:
if (val == 0) {
futex(&val, FUTEX_WAIT, 0, nullptr, nullptr, 0);
}
这会漏掉在 if 判断后、futex 进入等待前,另一个线程已把 val 改为非零并成功获取锁的情况——导致两个线程同时认为自己拿到了锁。
真正可行的模式是:先用原子操作尝试抢锁;失败则进入等待循环;唤醒后再次原子抢锁。
futex 的两种核心调用方式与对应场景
futex 系统调用有两大主力用法,选错会导致死锁或忙等:
-
FUTEX_WAIT:线程挂起等待某个内存地址的值变为期望值。必须传入当前预期值(expected),内核会原子地比较该地址当前值是否等于expected;若不等,立刻返回-EAGAIN,不挂起。 -
FUTEX_WAKE:唤醒最多nr个正在该地址上等待的线程。注意:它不关心地址当前值,只按等待队列唤醒——所以唤醒时机必须由用户态逻辑严格控制(比如只在释放锁时调用)。
别用 FUTEX_WAIT_BITSET 或 FUTEX_CMP_REQUEUE 开始——它们用于更复杂的同步结构(如条件变量、读写锁),对基础 Mutex 是过度设计,且易出错。
手写 Mutex 的最小可行状态机与关键代码片段
一个正确、轻量、无自旋退避的用户态 Mutex 只需一个 int 成员,但必须满足三个条件:
- 初始值为
0(表示未加锁) - 加锁时用
__atomic_load_n(&state, __ATOMIC_ACQUIRE)读取当前状态 - 加锁主循环中,用
__atomic_compare_exchange_n(&state, &expected, 1, false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)尝试设为1;失败则调用futex(&state, FUTEX_WAIT, 0, ...)等待 - 解锁时,先
__atomic_store_n(&state, 0, __ATOMIC_RELEASE),再futex(&state, FUTEX_WAKE, 1, ...)
示例关键逻辑(省略错误检查):
class futex_mutex {
int state = 0;
public:
void lock() {
int expected = 0;
while (!__atomic_compare_exchange_n(&state, &expected, 1, false,
__ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
if (expected == 1) {
futex(&state, FUTEX_WAIT, 1, nullptr, nullptr, 0);
expected = 0; // 重置,下次再试
}
}
}
void unlock() {
__atomic_store_n(&state, 0, __ATOMIC_RELEASE);
futex(&state, FUTEX_WAKE, 1, nullptr, nullptr, 0);
}
};
注意:这里 expected == 1 才等待,是因为我们只在发现已被占用时才挂起;如果 expected 是其他值(比如因信号中断被改写),应重新开始循环。
容易被忽略的四个坑
实际部署时,以下四点几乎必踩:
-
futex系统调用可能被信号中断,返回-EINTR,必须重试,不能直接退出等待循环 - 解锁时调用
FUTEX_WAKE前,必须确保state已设为0,且用__ATOMIC_RELEASE——否则唤醒可能发生在写入0之前,导致等待线程看到旧值而继续挂起 - 不要在加锁路径里做任何可能分配内存或调用 libc 锁的操作(比如
std::cout),否则可能死锁——你的 Mutex 正是用来保护这些资源的 -
futex地址必须是**对齐的、生命周期覆盖整个锁使用期的全局/静态/堆内存**;栈上变量地址一旦函数返回就失效,用它调用futex会导致内核访问非法地址(SEGV)
最麻烦的其实是调试:futex 错误通常不报明显错误,而是表现为随机死锁或唤醒丢失,建议初期用 strace -e trace=futex 观察系统调用序列是否符合预期。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











