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语言函数栈帧的创建和销毁详解

    这篇文章主要为大家详细介绍了C语言函数栈帧的创建和销毁,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-02-02
  • 如何用c语言完成俄罗斯方块小游戏

    如何用c语言完成俄罗斯方块小游戏

    这篇文章主要介绍了如何使用C语言开发一个简单的俄罗斯方块游戏,涵盖了游戏设计、数据结构、核心逻辑和实现步骤,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-12-12
  • C/C++ 函数原理传参示例详解

    C/C++ 函数原理传参示例详解

    这篇文章主要为大家介绍了C/C++ 函数原理传参示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • MATLAB实现五子棋游戏(双人对战、可悔棋)

    MATLAB实现五子棋游戏(双人对战、可悔棋)

    这篇文章主要为大家详细介绍了MATLAB实现五子棋游戏,可以进行双人对战、也可悔棋,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-06-06
  • C++多重继承引发的重复调用问题与解决方法

    C++多重继承引发的重复调用问题与解决方法

    这篇文章主要介绍了C++多重继承引发的重复调用问题与解决方法,结合具体实例形式分析了C++多重调用中的重复调用问题及相应的解决方法,需要的朋友可以参考下
    2018-05-05
  • C++实现LeetCode(151.翻转字符串中的单词)

    C++实现LeetCode(151.翻转字符串中的单词)

    这篇文章主要介绍了C++实现LeetCode(151.翻转字符串中的单词),本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-07-07
  • 深入剖析Android中init进程实现的C语言源码

    深入剖析Android中init进程实现的C语言源码

    这篇文章主要介绍了Android中init进程实现的C语言源码,init属性服务在安卓中属于系统的底层Linux服务,需要的朋友可以参考下
    2015-07-07
  • Qt采用线程以队列方式实现下发数据

    Qt采用线程以队列方式实现下发数据

    在C++中队列是一种常用的数据结构之一,一种特殊的线性表,一般采用先进先出的方式。本文主要为大家介绍了Qt如何以队列方式实现下发数据,感兴趣的可以了解一下
    2022-10-10
  • 一篇文章带你了解C语言内存对齐公式

    一篇文章带你了解C语言内存对齐公式

    这篇文章主要介绍了C语言内存对齐,包括内存对其的基本概念及用法,以及注意事项,并以实例形式加以说明,需要的朋友可以参考下,希望能给你带来帮助
    2021-08-08
  • vscode工程中c_cpp_properties.json文件作用详细说明

    vscode工程中c_cpp_properties.json文件作用详细说明

    c_cpp_properties.json是Visual Studio Code的一个配置文件,用于定义C/C++编译器的路径、默认包含路径和预处理器定义,这篇文章主要给大家介绍了关于vscode工程中c_cpp_properties.json文件作用详细说明的相关资料,需要的朋友可以参考下
    2024-08-08

最新评论