C++ 原子锁存器Atomic Flag实现自旋锁
C++11 引入了 std::atomic_flag,这是标准库中最轻量级的原子类型,专为实现无锁同步原语(如自旋锁)而设计。它只支持两个操作:测试并设置(test-and-set)和清除(clear),天然适合构建高效的自旋锁。
理解 std::atomic_flag 的基本特性
确认你的编译器支持 C++11 或更高标准(如 g++ -std=c++11)。
std::atomic_flag 的核心特点如下:
- 默认初始化状态为“清除”(即未锁定)。
- 不可复制、不可移动,只能通过特定成员函数操作。
- 保证无锁(lock-free):
is_lock_free()永远返回true。 - 仅提供两个操作:
test_and_set():原子地将标志设为“已设置”(locked),并返回设置前的值。clear():原子地将标志重置为“清除”(unlocked)。
注意:
std::atomic_flag没有load()或store()接口,不能直接读取当前状态而不修改它。
实现一个简单的自旋锁类
定义一个名为 SpinLock 的类,内部使用 std::atomic_flag 作为锁状态存储。
- 包含必要头文件:在代码开头添加
#include <atomic>。 - 声明类结构:使用
std::atomic_flag成员,并用ATOMIC_FLAG_INIT初始化(C++11)或{}(C++20 推荐)。 - 实现 lock() 方法:循环调用
test_and_set(),直到成功获取锁。 - 实现 unlock() 方法:调用
clear()释放锁。
#include <atomic>
class SpinLock {
private:
std::atomic_flag flag = ATOMIC_FLAG_INIT; // C++11 初始化方式
public:
void lock() {
while (flag.test_and_set(std::memory_order_acquire)) {
// 自旋等待:持续尝试获取锁
}
}
void unlock() {
flag.clear(std::memory_order_release);
}
};
在 C++20 中,可改用
std::atomic_flag flag{};,因为默认构造即为 cleared 状态。
正确使用内存顺序(Memory Order)
指定合适的内存顺序参数,以确保线程间操作的可见性和顺序性。
test_and_set()使用std::memory_order_acquire:确保后续内存操作不会被重排到锁获取之前。clear()使用std::memory_order_release:确保之前的所有内存操作在释放锁前对其他线程可见。
这种组合构成 获取-释放语义(acquire-release semantics),是实现互斥锁的标准做法。
在多线程环境中测试自旋锁
编写一个多线程测试程序,验证 SpinLock 能正确保护共享资源。
- 创建共享计数器:定义一个全局整型变量
counter,初始值为 0。 - 定义工作函数:多个线程反复对
counter加 1,每次操作前加锁,操作后解锁。 - 启动多个线程:例如创建 4 个线程,每个执行 10000 次加法。
- 等待所有线程结束:使用
join()。 - 检查最终结果:应等于线程数 × 每个线程的操作次数。
#include <iostream>
#include <thread>
#include <vector>
int counter = 0;
SpinLock spinlock;
void increment(int n) {
for (int i = 0; i < n; ++i) {
spinlock.lock();
++counter;
spinlock.unlock();
}
}
int main() {
const int num_threads = 4;
const int ops_per_thread = 10000;
std::vector<std::thread> threads;
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(increment, ops_per_thread);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Final counter: " << counter << std::endl;
// 输出应为 40000
return 0;
}
运行该程序,若输出为 40000,说明自旋锁正确防止了数据竞争。
优化:加入短暂让出 CPU 的策略
纯忙等待会浪费 CPU 资源。改进 lock() 方法,在每次失败后提示操作系统切换线程。
- 包含
<thread>头文件。 - 在自旋循环中调用
std::this_thread::yield()。
void lock() {
while (flag.test_and_set(std::memory_order_acquire)) {
std::this_thread::yield(); // 让出当前时间片
}
}
yield()建议调度器切换到其他就绪线程,降低 CPU 占用,尤其在线程数超过核心数时效果明显。
与 std::mutex 对比的关键差异
| 特性 | SpinLock(基于 atomic_flag) |
std::mutex |
|---|---|---|
| 锁类型 | 自旋锁(忙等待) | 阻塞锁(挂起线程) |
| 适用场景 | 临界区极短、低竞争 | 临界区较长、可能高竞争 |
| 上下文切换 | 无 | 有(系统调用开销) |
| CPU 占用 | 高(等待时持续占用) | 低(等待时释放 CPU) |
| 实现复杂度 | 极简(几行代码) | 复杂(依赖 OS 原语) |
选择自旋锁仅当临界区执行时间远小于线程切换开销(通常几十纳秒内)。
注意事项与常见错误
避免以下典型陷阱:
- 不要递归加锁:
SpinLock不可重入。同一线程连续调用lock()会导致死锁。 - 不要忘记 unlock():务必在临界区结束后调用
unlock(),否则其他线程永远无法进入。 - 异常安全:若临界区内抛出异常,可能导致锁未释放。应使用 RAII 封装。
使用 RAII 封装锁操作,确保异常安全:
class SpinLockGuard {
SpinLock& lock_;
public:
explicit SpinLockGuard(SpinLock& l) : lock_(l) {
lock_.lock();
}
~SpinLockGuard() {
lock_.unlock();
}
// 禁止复制
SpinLockGuard(const SpinLockGuard&) = delete;
SpinLockGuard& operator=(const SpinLockGuard&) = delete;
};
在临界区开始处创建 SpinLockGuard 对象,作用域结束时自动解锁。
编译与运行建议
使用现代 C++ 编译器并启用优化:
g++ -std=c++17 -O2 -pthread spinlock_demo.cpp -o spinlock_demo
-O2:启用优化,确保原子操作生成高效汇编(如 x86 的xchg或cmpxchg)。-pthread:链接 POSIX 线程库(Linux/macOS 必需)。
验证原子操作是否真正无锁:可通过 static_assert(flag.is_lock_free(), "Not lock-free!"); 断言(但 atomic_flag 永远满足)。
自旋锁适用于极短临界区的高性能场景,std::atomic_flag 提供了最底层、最高效的构建块。正确使用内存顺序和 RAII 封装,可避免常见并发错误。

暂无评论,快来抢沙发吧!