推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[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_weak 和 compare_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 的底层实现通常分为两步:
- Load-Linked (LL) / Load-Exclusive (LDREX): 从内存地址加载值,同时CPU会“监视”这个地址。
- Store-Conditional (SC) / Store-Exclusive (STREX): 只有在被“监视”的地址在此期间没有被其他线程修改的情况下,存储才会成功。
如果在 LL 和 SC 之间,发生了任何其他事件(比如另一个线程访问了同一个缓存行,甚至只是一个无关的中断),都可能导致 SC 操作失败,即使该内存地址的值其实并没有改变!这就是 compare_exchange_weak 产生“伪失败”的硬件根源。
我使用的 Ubuntu 虚拟机的 CPU 架构是 x86-64:
| 维度 | x86 / x86-64 | ARM |
|---|---|---|
| 公司 | Intel 设计,AMD 等兼容 | ARM Ltd. 设计,授权给各大厂商 |
| 指令集 | CISC(复杂指令集) | RISC(精简指令集) |
| 常用设备 | PC、服务器、笔记本 | <

最低0.47元/天 解锁文章
1433

被折叠的 条评论
为什么被折叠?



