C/C++原子操作与atomic CAS底层实现原理

原子操作

在c++中标准库也提供了原子操作的模板类,在头文件#include< atomic>中

定义

计算机执行的最小单位就是指令,即CPU一次只能执行一条指令。

假定有两个操作A 和B,如果从执行A 的线程来看,当另一个线程执行B 时,要么将B 全部执行完,要么完全不执行B,那么A 和B 对彼此来说是原子的。

理想很丰满,现实很骨感.一句c语言"i++"的操作,经过编译翻译成汇编语言后成了三句汇编代码

//c语言 ==> i++
//汇编语言 ==> intel风格汇编
__asm__{
   
   
    mov eax [i]    //将变量i的值取出到cpu寄存器eax中
    inc eax        //eax自增1
    mov [i] eax    //将寄存器eax的值写入内存变量i的地址中
}

AI 代码解读

在单线程下这些汇编代码执行正常,但是在多线程环境下,这三条汇编指令可能才执行了一句或者两句,就因为cpu时间片结束了导致线程切换,被其他的线程获取值后修改了,导致变量被重复覆盖了

i++是三条指令,所以可能执行完第一条被其他线程抢了时间片,造成结果跟预期不一致问题。
经典的如10个线程每个线程执行1000次i++操作,理想情况下预期是10000,实际上并不一定能到10000,也可能是九千多.

这是AT&T的汇编风格

在这里插入图片描述

实现

那有没有什么方法可以避免这一步情况的发生呢,当然,除了常见的加锁外,还有一个粒度很小的方式,那就是原子操作,原子操作需要硬件的支持,不过大部分的电脑都是支持的.

通过汇编指令lock,可以使这三句汇编代码变成一句不可分割的指令(原理是通过lock指令锁住cpu总线(计算机组成原理的知识))

用嵌入汇编实现自增的原子操作

int inc(int* value, int add) {
   
   
    int old;
    __asm__ volatile (
         "lock; xaddl %2, %1;"   // 指令1:lock; 指令2: xaddl, 操作数占位符:%1, %2
         : "=a" (old)            // 输出:结果放入通用寄存器eax
         : "m" (*value), "a" (add)   // 输入:操作数1(内存),操作数2(寄存器eax)
         : "cc", "memory"            // 编译方式,内存
     );
    return old;
}

AI 代码解读

在这里插入图片描述

CAS

定义

CAS(Compare and Swap)比较并交换,CAS是一种无锁算法,CAS有3个操作数,传入旧值跟新值让compare去比较内存中已经存在的旧值,如果传入进来的旧值跟内存中的旧值一致那就把传入进来的新增修改,如果不相等则采用自旋的方式拿到内存中的旧值在再次进行比较,自旋可以可以理解为自旋锁机制含义

实现

cas是硬件层面提供的操作

CAS 的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS 就是在一个循环里不断的做cas 操作,直到成功为止。

在这里插入图片描述

问题

1.ABA问题

因为CAS 需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。

你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA 问题。

如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。

解决方案: 可以采用AtomicMarkableReference,AtomicStampedReference进控制

描述:

1. AtomicStampedReference 是通过 int 类型的版本号,每次修改版本号都会增加,cas操作发现版本号不一致就会返回
1. 而 AtomicMarkableReference 是通过 boolean 型的标识来判断数据是否有更改过。

AI 代码解读

既然有了 AtomicStampedReference 为啥还需要再提供 AtomicMarkableReference 呢,在现实业务场景中,不关心引用变量被修改了几次,只是单纯的关心是否更改过。

2.开销问题

由于存在并发情况同时修改值,如果值被改过了,就会重新获取内存中的值进行比较,在此期间如果一直修改不成功,会导致做死的循环,会有性能损耗

3.只能保证一个共享变量的原子操作

每次更新操作我只能更新一个值,即一个CAS指令,如果有连个CAS指令那就时独立的了不能保证原子操作

解决方案:

1. 把多个共享变量合并成一个共享变量来操作。
1. 加锁解决

AI 代码解读

c++语言层面

在c++语言层面提供了cas操作,在头文件#include< atomic>中

  1. compare_exchange_weak
  2. compare_exchange_strong
bool compare_exchange_weak( T& expected, T desired,
                            std::memory_order success,
                            std::memory_order failure ) noexcept;
bool compare_exchange_weak( T& expected, T desired,
                            std::memory_order success,
                            std::memory_order failure ) volatile noexcept;
//(2)    (C++11 起)
bool compare_exchange_weak( T& expected, T desired,
                            std::memory_order order =
                                std::memory_order_seq_cst ) noexcept;
bool compare_exchange_weak( T& expected, T desired,
                            std::memory_order order =
                                std::memory_order_seq_cst ) volatile noexcept;
//(3)    (C++11 起)
bool compare_exchange_strong( T& expected, T desired,
                              std::memory_order success,
                              std::memory_order failure ) noexcept;
bool compare_exchange_strong( T& expected, T desired,
                              std::memory_order success,
                              std::memory_order failure ) volatile noexcept;
//(4)    (C++11 起)
bool compare_exchange_strong( T& expected, T desired,
                              std::memory_order order =
                                  std::memory_order_seq_cst ) noexcept;
bool compare_exchange_strong( T& expected, T desired,
                              std::memory_order order =
                                  std::memory_order_seq_cst ) volatile noexcept;

AI 代码解读

而compare_exchange_weak和compare_exchange_strong则是著名的CAS(compare and set)。参数会要求在这里传入期待的数值和新的数值。它们对比变量的值和期待的值是否一致,如果是,则替换为用户指定的一个新的数值。如果不是,则将变量的值和期待的值交换。

weak版本的CAS允许偶然出乎意料的返回(比如在字段值和期待值一样的时候却返回了false),不过在一些循环算法中,这是可以接受的。通常它比起strong有更高的性能。

C/C++原子操作与atomic CAS底层实现原理-阿里云开发者社区

### CAS锁在原子操作中的实现方式及区别 #### 一、CAS实现原理 CAS(Compare-And-Swap)是一种基于硬件支持的原子操作,其核心思想是在不使用显式锁的情况下完成对共享变量的安全更新。具体来说,CAS 操作包含三个参数:内存地址 V、期望值 E 和新值 N。如果内存地址 V 当前存储的值等于期望值 E,则将其更新为新值 N;否则不做任何更改。 这一过程由底层硬件指令保障其原子性,在多核处理器环境中能够高效运行而不引入额外的上下文切换开销[^1]。例如,在 x86 架构下,`cmpxchg` 指令实现CAS 功能。 ```java // JavaAtomic 类利用了 Unsafe 方法封装了 CAS 操作 public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } ``` 上述代码展示了如何通过 `Unsafe` 对象调用本地方法来实现 CAS 操作。 #### 二、锁机制的实现原理 相比之下,传统的锁机制则是通过操作系统内核的支持或用户态库函数实现的一种同步手段。当某个线程尝试获取锁时,若该锁已被其他线程持有,则请求者会被阻塞直到锁释放为止。常见的锁有互斥量(Mutex)、信号量(Semaphore)以及更高级别的读写锁等。 以 Mutex 为例,其实现通常依赖于条件变量和等待队列结构。一旦进入临界区,当前线程便独占资源使用权直至退出临界区并解锁[^2]。 ```c++ std::mutex mtx; void critical_section() { std::lock_guard<std::mutex> lock(mtx); // 自动管理锁生命周期 // 访问受保护的数据... } ``` 这段 C++ 示例演示了标准模板库中 mutex 的简单应用,其中 `std::lock_guard` 负责自动加锁解锁。 #### 三、两者的主要区别 | 特性 | CAS | 锁 | |---------------------|---------------------------------------|------------------------------------| | **性能影响** | 高效,尤其在线程冲突较少场景 | 可能引发频繁上下文切换 | | **适用范围** | 单个共享变量 | 复杂业务逻辑或多变量组合 | | **异常情况处理** | 存在 ABA 问题 | 不易出现类似问题 | | **死锁风险** | 不存在 | 若设计不当可能导致死锁 | 值得注意的是,尽管 CAS 在大多数情况下表现优异,但它并非万能解决方案。例如面对 ABA 问题时需借助附加信息如时间戳加以规避[^4];而对于涉及多个独立状态变化的需求,则往往仍需回归传统锁定策略才能确保整体一致性[^3]。 #### 四、总结 综上所述,CAS 技术凭借低延迟特性成为现代高性能并发框架不可或缺的一部分,但在实际开发过程中也应充分权衡各种因素合理选用工具。对于简单的计数器增减或是标志位翻转这样的单一对象操控而言,优先推荐采用 CAS 方案减少不必要的争用损耗;然而针对复杂事务型任务或者存在潜在竞态隐患的地方,则建议综合考量包括但不限于重入锁在内的多种防护措施。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值