【基础组件】手搓自旋锁(C++ 版本)—— 原子操作的最简单使用案例

推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接

介绍

我花费了 4 天的时间去思考撰写了一篇文章,专门介绍了 CPU 架构与原子操作的关系(原文 链接 在这)。在这篇文章里,我介绍过 CAS (compare-and-swap)原子操作,它是线程竞争公共变量失败后代价很低的函数(他直接返回布尔值 false,并且更新目标的左值取值),不过更重要的是他适合作线程的自旋锁,也就是本篇文章的主题。

什么是自旋锁

自旋锁是一种忙等待(Busy-Wait) 的锁机制。当一个线程尝试获取一个已经被占用的自旋锁时,它不会立即放弃 CPU,而是会在一个循环中不断地检查锁的状态,直到锁变得可用为止。

它的行为就像你在厕所门口发现里面有人,你不是去找个地方坐下(让出CPU),而是在门前等待(循环检查),直到里面的人出来为止。

自旋锁的核心通常由一个原子标志(如 std::atomic 或 std::atomic_flag)实现,最常用的是 CAS(Compare-And-Swap) 操作。CAS 操作失败了会立刻返回,而 CAS 操作成功抢到公共变量的内存就在几个 “时钟周期” 之内完成操作,然后就把公共资源分给其他 CPU 核心里的线程。在此期间,竞争公共变量失败的线程会进入短暂的死循环,他们是不会让出 CPU 的。一旦线程自旋锁解开,其他线程会立刻争先进入运算,抢夺成功后再把其他线程排斥开来。这是不会有上下文切换的消耗的(与互斥锁相比)。

使用自旋锁的关键是,锁住的资源的运算过程必须是极其短暂的,这样才会相对于互斥锁有优势。(互斥锁的优势在于执行运算时间较长的任务,因为竞争失败的线程是会让出 CPU 的,不会强行霸占 CPU 并且赖在那里作无限死循环。那么节省下来的计算机 CPU 资源就可以用来做其他任务了。)

再解 CAS 原子操作

compare_exchange_weakcompare_exchange_strong 两个原子操作都是 CAS 操作

#include <atomic>  //	头文件

atomic<T> 原子对象(某个 T 类型的变量)   ———— 这是拷贝原变量,再封装起来成原子对象。

原子对象.compare_exchange_weak(T& expected, T desired,success-mem-order, failure-mem-order)
原子对象.compare_exchange_strong(T& expected, T desired,success-mem-order, failure-mem-order)

对于原子操作来说(针对原子对象的操作,原子对象本质不是比较的对象,需要拆包,自己拆包后跟自己比,有可能其他线程抢先一步更改了)

bool compare_exchange_strong(expected, desired, succ,fail)

T& expected,                 // ① 输入:你猜的旧值;输出:现场看到的旧值(左值引用)
T desired,                   // ② 如果猜对了,要写入的新值
std::memory_order succ = std::memory_order_seq_cst,  // ③ 成功时的内存序(这个变量会否发布)
std::memory_order fail  = std::memory_order_seq_cst  // ④ 失败时的内存序(是否)

返回值的含义
		true → 比较相等且已写入 desired。
		false → 比较不等,什么都没改,expected 已被更新为现场值。

两者的功能完全相同:如果原子变量的当前值等于期望值 expected,则将其替换为 desired 值并返回 true;否则,将当前值加载到 expected 中并返回 false。

它们的唯一区别在于对待伪失败的态度。

  • compare_exchange_strong: 绝对强保证。它只有在条件确实不满足(当前值 != expected)时才返回 false。这是我们通常理解的、逻辑上的“失败”。
  • compare_exchange_weak: 允许伪失败。即使条件实际上满足(当前值 == expected),它也可能偶尔“抽风”一次而返回 false。这种“撒谎”的行为就是所谓的“伪失败”。

为什么会存在“伪失败”?
这主要是为了在某些硬件平台上(特别是 LL/SC 架构的CPU,如ARM, PowerPC, MIPS)实现更高的性能。

在这些平台上,compare_exchange 的底层实现通常分为两步:

  1. Load-Linked (LL) / Load-Exclusive (LDREX): 从内存地址加载值,同时CPU会“监视”这个地址。
  2. Store-Conditional (SC) / Store-Exclusive (STREX): 只有在被“监视”的地址在此期间没有被其他线程修改的情况下,存储才会成功。

如果在 LLSC 之间,发生了任何其他事件(比如另一个线程访问了同一个缓存行,甚至只是一个无关的中断),都可能导致 SC 操作失败,即使该内存地址的值其实并没有改变!这就是 compare_exchange_weak 产生“伪失败”的硬件根源。

我使用的 Ubuntu 虚拟机的 CPU 架构是 x86-64:

<
维度 x86 / x86-64 ARM
公司 Intel 设计,AMD 等兼容 ARM Ltd. 设计,授权给各大厂商
指令集 CISC(复杂指令集) RISC(精简指令集)
常用设备 PC、服务器、笔记本
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值