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

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

原子操作

在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的地址中
}

在单线程下这些汇编代码执行正常,但是在多线程环境下,这三条汇编指令可能才执行了一句或者两句,就因为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;
}

在这里插入图片描述

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 型的标识来判断数据是否有更改过。

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

2.开销问题

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

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

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

解决方案:

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

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;

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

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

目录
相关文章
|
2月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
2月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
2月前
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
2月前
|
C++
C++番外篇——虚拟继承解决数据冗余和二义性的原理
C++番外篇——虚拟继承解决数据冗余和二义性的原理
45 1
|
6月前
|
存储 自然语言处理 安全
C++ STL标准库 《string原理与实战分析》
C++ STL标准库 《string原理与实战分析》
99 0
|
7月前
|
小程序 编译器 Linux
C++ 异常原理:以一个小程序为例
作者在调查某个 bug 时涉及到 C++ 异常,借此机会以本文把 C++ 异常机制梳理清楚供大家参考。
|
3月前
|
存储 安全 C++
C++ 原子变量atomic variable
原子变量是 C++11 引入的一种同步机制,用于多线程环境中的无锁、线程安全操作。其操作不可分割,避免了数据竞争和不一致问题。原子变量位于 `&lt;atomic&gt;` 头文件中,支持多种类型如 `std::atomic&lt;T&gt;` 和特化类型。基本用法包括定义原子变量、加载、存储、交换及比较交换操作。内存顺序(如 `std::memory_order_seq_cst`)用于控制内存访问顺序和可见性,适用于不同场景。原子变量常用于线程安全的计数器和标志位等。
|
5月前
|
算法 编译器 C++
开发与运维线程问题之在C++的原子操作中memory_order如何解决
开发与运维线程问题之在C++的原子操作中memory_order如何解决
51 2
|
6月前
|
安全 程序员 C++
C++一分钟之-原子操作与线程安全
【6月更文挑战第27天】**C++的`std::atomic`提供线程安全的原子操作,解决多线程数据竞争。涵盖原子操作概念、应用、问题与对策。例如,用于计数器、标志位,但选择数据类型、内存顺序及操作组合需谨慎。正确使用能避免锁,提升并发性能。代码示例展示自旋锁和线程安全计数。了解并恰当运用原子操作至关重要。**
104 1
|
6月前
|
C++
C/C++内存管理(2):`new`和`delete`的实现原理
C/C++内存管理(2):`new`和`delete`的实现原理