C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++自旋锁

C++自旋锁的实现示例

作者:码上睡觉

自旋锁是一种非阻塞锁,线程在获取锁失败时会忙等待而不是挂起,它适用于锁持有时间极短的场景,可以在用户态运行,避免系统调用开销,下面就来详细的介绍一下C++自旋锁的使用,感兴趣的可以了解一下

一、 什么是自旋锁?

核心定义:

自旋锁是一种非阻塞锁。当线程尝试获取锁失败时,它不会挂起(阻塞/让出 CPU),而是会在一个死循环中持续检查(忙等待 / Busy-Waiting)锁是否被释放。

直观隐喻

互斥锁 (Mutex):你去洗手间,发现门锁了。你回到座位上睡觉。等里面的人出来了,管理员把你叫醒,你再去上。

自旋锁 (Spinlock):你去洗手间,发现门锁了。你站在门口,每隔 0.1 秒就敲门问:“好了没?好了没?”,直到里面的人出来。

二、 为什么需要自旋锁?(底层视角)

您可能会问:“让线程空转浪费 CPU,这不是很傻吗?”

要理解它的价值,必须看**上下文切换(Context Switch)**的成本。

  1. Mutex 的成本
  1. Spinlock 的优势

结论:自旋锁适用于**“锁持有时间极短”**的场景。

三、 C++ 中的自旋锁实现

C++ 标准库并没有直接提供 std::spinlock(C++20 只有 std::atomic_flag),我们需要利用原子操作自己实现。

1. 最基础的实现:std::atomic_flag

这是 C++ 中唯一保证**无锁(Lock-Free)**的数据类型。

#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
 
class SpinLock {
private:
// atomic_flag 只有两个状态:set (true) 和 clear (false)
// ATOMIC_FLAG_INIT 初始化为 false
std::atomic_flag flag = ATOMIC_FLAG_INIT;
 
public:
void lock() {
    // test_and_set(): 
    // 1. 读取当前值
    // 2. 将值设为 true
    // 3. 返回旧值
    // 这是一个原子操作 (RMW: Read-Modify-Write)
 
    // 如果返回 true,说明之前已经是 true (被别人锁了),则一直循环 (自旋)
    // memory_order_acquire: 保证获得锁之后的读写操作不会重排到加锁之前
    while (flag.test_and_set(std::memory_order_acquire)) {
        // 这里是自旋区 (Spinning)
        // 可以在这里加 "CPU pause" 指令优化(后面会讲)
    }
}
 
void unlock() {
    // 清除标志,设为 false
    // memory_order_release: 保证解锁之前的读写操作全部完成
    flag.clear(std::memory_order_release);
}
};
 
// 使用示例(配合 lock_guard 满足 RAII)
SpinLock sl;
void worker() {
    // std::lock_guard 需要类满足 BasicLockable (有 lock/unlock 方法)
    std::lock_guard<SpinLock> guard(sl); 
    // 临界区...
}

2. 通用实现:std::atomic<bool>

功能类似,但 atomic<bool> 可以提供更多 API(比如 load 查看状态),只是在极老的硬件上可能不是 Lock-Free 的(虽然现在几乎都是)。

class SpinLockBool {
std::atomic<bool> locked{false};
public:
void lock() {
    bool expected = false;
    // CAS (Compare And Swap)
    // 尝试把 locked 从 false 改成 true
    // 如果 locked 是 true (被锁),compare_exchange_weak 返回 false,继续循环
    while (!locked.compare_exchange_weak(expected, true, std::memory_order_acquire)) {
        expected = false; // CAS 失败后 expected 会被改成当前值(true),重置为 false 再次尝试
    }
}
void unlock() {
    locked.store(false, std::memory_order_release);
}
};

四、 致命陷阱与性能优化(C++ 高阶)

在实现高性能组件(如内存池)时,直接用 while(flag.test_and_set()) 会带来严重的性能问题。

1. 缓存一致性风暴 (Cache Coherence Storm / Bus Contention)

现象:多个线程在一个原子变量上疯狂 CAS(写操作)。

原理:根据 CPU 的 MESI 协议,当一个核修改原子变量时,必须让其他核的 Cache Line 失效。如果 10 个线程同时自旋,锁变量所在的 Cache Line 会在 CPU 核心之间疯狂“跳来跳去”,导致总线流量爆炸,甚至拖慢其他不相关线程的速度。

解决Test-Test-and-Set (TTAS) 模式。

2. CPU 流水线空转

现象while 循环是一个极紧密的指令序列,CPU 流水线会全速运行,产生大量热量并消耗电力。

解决:CPU Pause 指令。

在 x86 架构下,使用 _mm_pause() 指令(SSE2 扩展)。

  1. 它告诉 CPU “我在自旋”,让 CPU 稍微降低流水线派发速度,节能降温。
  2. 它可以避免退出循环时的内存顺序冲突惩罚。

优化后的 C++ 代码:

#include <atomic>
#include <immintrin.h> // for _mm_pause
 
class OptimizedSpinLock {
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() {
    while (flag.test_and_set(std::memory_order_acquire)) {
        // 在自旋期间...
        while (flag.test(std::memory_order_relaxed)) { // 先只读 (Test)
            // 告诉 CPU 稍微休息一下,不要全速空转
            #if defined(__x86_64__) || defined(_M_X64)
            _mm_pause(); 
            #endif
            // 如果是 ARM 架构,可以用 __yield() 或 asm("yield")
        }
    }
}
void unlock() {
    flag.clear(std::memory_order_release);
}
};

3. 优先级反转与死锁

到此这篇关于C++自旋锁的实现示例的文章就介绍到这了,更多相关C++自旋锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文