C++自旋锁的实现示例

 更新时间:2026年02月13日 11:37:30   作者:码上睡觉  
自旋锁是一种非阻塞锁,线程在获取锁失败时会忙等待而不是挂起,它适用于锁持有时间极短的场景,可以在用户态运行,避免系统调用开销,下面就来详细的介绍一下C++自旋锁的使用,感兴趣的可以了解一下

一、 什么是自旋锁?

核心定义:

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

直观隐喻

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

  • 开销:睡觉(切换上下文)和被叫醒(调度)很累。

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

  • 开销:你一直站着(占用 CPU),哪里也去不了。但是一旦门开了,你零延迟冲进去。

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

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

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

  1. Mutex 的成本
  • std::mutex 拿不到锁时,线程会陷入内核态(Kernel Mode)。
  • OS 需要保存当前线程的寄存器、栈指针,刷新 TLB(页表缓存),然后调度另一个线程。
  • 这个过程大约需要 3 ~ 10 微秒(在现代 CPU 上)。
  1. Spinlock 的优势
  • 如果您的临界区代码执行时间极短(比如只是做一个 pNext = node; 的链表操作),耗时可能只有 0.01 微秒
  • 为了等待 0.01 微秒的任务,去花费 5 微秒切换线程,是亏本生意
  • 自旋锁全程在**用户态(User Mode)**运行,完全没有系统调用开销。

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

三、 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) 模式。

  • 先用 load (读) 检查是否被释放(读操作不独占 Cache Line)。
  • 只有读到 false 时,才尝试 CAS (写)。

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. 优先级反转与死锁

  • 场景:如果一个低优先级线程拿到了自旋锁,但因为被 OS 调度走了(时间片到了),一个高优先级线程被调度进来,尝试获取同一个自旋锁。
  • 结果:高优先级线程因为是自旋(忙等),它不让出 CPU,导致低优先级线程永远得不到 CPU 来执行解锁操作。于是死锁
  • 对策:在自旋锁中,如果自旋超过一定次数,必须使用 std::this_thread::yield() 主动让出时间片。

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

相关文章

  • C语言 解压华为固件的实例代码

    C语言 解压华为固件的实例代码

    这是解压华为固件(update.app)的C语言。。其实这也是我翻115翻出来的。。。
    2013-08-08
  • 浅谈c语言中一种典型的排列组合算法

    浅谈c语言中一种典型的排列组合算法

    下面小编就为大家带来一篇浅谈c语言中一种典型的排列组合算法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05
  • VC++获得当前进程运行目录的方法

    VC++获得当前进程运行目录的方法

    这篇文章主要介绍了VC++获得当前进程运行目录的方法,可通过系统函数实现该功能,是非常实用的技巧,需要的朋友可以参考下
    2014-10-10
  • Qt 使用Poppler实现pdf阅读器的示例代码

    Qt 使用Poppler实现pdf阅读器的示例代码

    下面小编就为大家分享一篇Qt 使用Poppler实现pdf阅读器的示例代码,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-01-01
  • 纯C语言实现五子棋

    纯C语言实现五子棋

    本文给大家分享的是去年制作的一个纯C语言实现的五子棋的代码,虽然没有带漂亮的界面,还是推荐给大家,有需要的小伙伴可以参考下。
    2015-03-03
  • C语言二叉树的三种遍历方式的实现及原理

    C语言二叉树的三种遍历方式的实现及原理

    这篇文章主要介绍了C语言二叉树的三种遍历方式的实现及原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-07-07
  • c++之time_t和struct tm及时间戳的正确使用方式

    c++之time_t和struct tm及时间戳的正确使用方式

    C++中处理时间的常用数据类型有time_t和struct tm,time_t通常用来表示时间戳,即从1970年1月1日至今的秒数,struct tm是一个结构体,用来存储年、月、日、时、分、秒等信息,时间戳可以通过gmtime()转换为struct tm类型,反之亦然
    2024-10-10
  • wxWidgets实现无标题栏窗口拖动效果

    wxWidgets实现无标题栏窗口拖动效果

    这篇文章主要为大家详细介绍了wxWidgets实现无标题栏窗口拖动效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-02-02
  • 详细分析C++ 多态和虚函数

    详细分析C++ 多态和虚函数

    这篇文章主要介绍了C++ 多态和虚函数的相关资料,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-07-07
  • 如何求连续几个数之和的最大值

    如何求连续几个数之和的最大值

    本篇文章是对如何求连续几个数之和的最大值进行了详细的分析介绍,需要的朋友参考下
    2013-05-05

最新评论