文章目录

C++ 原子锁存器Atomic Flag实现自旋锁

发布于 2026-04-03 18:42:11 · 浏览 2 次 · 评论 0 条

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 作为锁状态存储。

  1. 包含必要头文件:在代码开头添加 #include <atomic>
  2. 声明类结构:使用 std::atomic_flag 成员,并用 ATOMIC_FLAG_INIT 初始化(C++11)或 {}(C++20 推荐)。
  3. 实现 lock() 方法:循环调用 test_and_set(),直到成功获取锁。
  4. 实现 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 能正确保护共享资源。

  1. 创建共享计数器:定义一个全局整型变量 counter,初始值为 0。
  2. 定义工作函数:多个线程反复对 counter 加 1,每次操作前加锁,操作后解锁。
  3. 启动多个线程:例如创建 4 个线程,每个执行 10000 次加法。
  4. 等待所有线程结束:使用 join()
  5. 检查最终结果:应等于线程数 × 每个线程的操作次数。
#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() 方法,在每次失败后提示操作系统切换线程。

  1. 包含 <thread> 头文件
  2. 在自旋循环中调用 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 原语)

选择自旋锁仅当临界区执行时间远小于线程切换开销(通常几十纳秒内)。


注意事项与常见错误

避免以下典型陷阱:

  1. 不要递归加锁SpinLock 不可重入。同一线程连续调用 lock() 会导致死锁。
  2. 不要忘记 unlock():务必在临界区结束后调用 unlock(),否则其他线程永远无法进入。
  3. 异常安全:若临界区内抛出异常,可能导致锁未释放。应使用 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 的 xchgcmpxchg)。
  • -pthread:链接 POSIX 线程库(Linux/macOS 必需)。

验证原子操作是否真正无锁:可通过 static_assert(flag.is_lock_free(), "Not lock-free!"); 断言(但 atomic_flag 永远满足)。


自旋锁适用于极短临界区的高性能场景,std::atomic_flag 提供了最底层、最高效的构建块。正确使用内存顺序和 RAII 封装,可避免常见并发错误。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文