C++多线程:Atomic原子类与CAS锁详解(十)

1、原子操作的概念

什么是原子操作:

  • 原子被认为是构成物质最小的单位,是不可分割的一个东西。
  • 而在程序中原子操作被认为是不可分割的一个步骤或者指令
  • 其实我们很简单的程序,在高级语言中被认为是一个步骤的操作,编译成汇编指令之后却是一个非原子操作,即生成多次指令集操作
  • 而多个指令集组合构成的一个高级语言的操作在执行的时候需要一条一条指令去执行
  • 每条执行完毕是有可能被操作系统调度中断,因此这就会导致线程数据安全问题。
  • 因此为了解决这个非线程安全的操作问题,可以引入一系列的方式:互斥锁(mutex)、同步、信号量、atomic原子操作等
2、高级语言代码与汇编指令

首先看一段函数代码

  • objdump -DC add.o:先编译成.o文件,然后执行这个命令进行反汇编
  • 左边是一个函数,简单的进行a++的执行并且返回
  • 右边是这个函数对应的汇编指令集,看不懂没关系,待会就会懂个七七八八了。
int add(int a){
a++;
return a;
}
0000000000000000 <add(int)>:
push %rbp
mov %rsp,%rbp
/*
这三个指令是完成一个a++操作的所有指令
mov %edi,-0x4(%rbp)
addl $0x1,-0x4(%rbp)
mov -0x4(%rbp),%eax
*/
pop %rbp
retq
  • 首先进行一些名词解释:
    • 首先一个函数的调用都是一个一个的栈帧,而一个栈帧由rbp和rsp分别指向这个栈帧的底部(栈底)和顶部(栈顶)
    • rbp寄存器:栈帧基址指针(Base Pointer),指向这个栈帧的底部(栈底)
    • rsp寄存器:栈指针(Stack Pointer),指向这个栈帧的顶部(栈顶)
    • mov:移动指令
    • addl:加法指令
    • pop:弹出栈指令
    • retq:返回return对应的指令
2.1、函数的调用过程

假设存在一个main函数主体和一个函数add,在main中调用add函数,栈帧的变化过程看下图
在这里插入图片描述

2.2、汇编指令集解析
0000000000000000 <add(int)>:
1   push   %rbp
2   mov    %rsp,%rbp
3   mov    %edi,-0x4(%rbp)
4   addl   $0x1,-0x4(%rbp)
5   mov    -0x4(%rbp),%eax
6   pop    %rbp
7  retq
  • 首先看第一个问题,rbp怎么跳转上去的:这一步就有点像爬楼梯的左脚(rbp)踩右脚(rsp)

    • 创建一个新的栈帧(函数调用)时会将当前栈帧的值存放在栈帧上,对应指令:push %rbp(1)

    • 创建一个新的栈帧,这里创建一个新的栈帧其实很简单,只需要从rsp的位置向上开始创建即可,因此对应指令:mov %rsp,%rbp,意思将rsp的值赋给rbp(2)

    • 再将参数a的值存放在%rbp-0x4的地方,对应指令:mov %edi,-0x4(%rbp)(3)

    • 然后再将1这个值加到%rbp-0x4的地方,对应指令:addl $0x1,-0x4(%rbp)(4)

    • 最后将%rbp-0x4这个地址存放的值放到%eax寄存器中,对应指令:mov -0x4(%rbp),%eax(5)

    • 计算完毕开始弹出新的栈帧(add函数)的栈帧首地址(6)

    • 返回%eax寄存器中的值(7)

  • 可以看到a++这一个步骤需要3条汇编指令,因此a++操作在高级语言中并不是一个原子操作,在多线程下并不安全。

在这里插入图片描述

3、atomic原子类
  • 经过上面的分析,可以清楚的发现多线程情况下的a++并不是安全稳定的,核心原因就是其是一个非原子操作。

  • C++11中提供了atomic头文件,和对应的atomic原子类操作,用于可以保证基础数据类型的这种操作的多线程数据安全的稳定性。

  • atomic原子类只能操作基本的数据类型,对于自定义的数据类型是无法使用的,这一点底层源码都有展示。

3.1、非线程安全操作演示

执行了五次没有一次数据是正确的,可能是我脸太黑了,也有可能是cpu太繁忙,其实最主要的是因为线程不安全。

#include <iostream>
#include <atomic>
#include <thread>

int a;
void thread_func()
{
    for(int i = 0;i < 100000;i++){
        a++;
    }
}

int main()
{
    for(int i = 0;i < 5;i++){
        a = 0;
        std::thread thread1(thread_func);
        std::thread thread2(thread_func);
        thread1.join();
        thread2.join();
        std::cout << "第" << i << "次结束时: a = " << a << std::endl;
    }
    return 0;
}

在这里插入图片描述

3.2、atomic原子类使用
  • 解决这个方法很多,这里主要使用atomic原子操作去解决这个问题,对于使用一些其他手段就不在讨论的范围内(如互斥锁、同步等)
  • 只需要将int类型的a定义成一个atomic原子int类型的a即可将问题解决。
  • 对比mutex和一些其他的线程安全手段,atomic的最大优势就在于其性能优越、效率更高。
std::atomic<int> a;
void thread_func()
{
    for(int i = 0;i < 100000;i++){
        a++;
    }
}

int main()
{
    for(int i = 0;i < 5;i++){
        a = {0};
        std::thread thread1(thread_func);
        std::thread thread2(thread_func);
        thread1.join();
        thread2.join();
        std::cout << "第" << i << "次结束时: a = " << a << std::endl;
    }
    return 0;
}
3.3、atomic原子类支持的操作
  • atomic原子类并不是所有的操作都支持,存在一些特殊的情况并不支持原子操作
  • 首先需要知道atomic原子类继承基类__atomic_base类,基类中有所有支持操作的源码,如下
  • 而所谓的a = a + 1并不在支持的范围内,这个操作被认为是一个普通的操作,不会调用运算符的重载
//赋值重载
__int_type operator=(__int_type __i) volatile noexcept;

// 后置++
__int_type operator++(int) volatile noexcept;

// 后置--
__int_type operator--(int) volatile noexcept;

// 前置++
__int_type operator++() volatile noexcept;

// 前置--
__int_type operator--() volatile noexcept;

// +=
__int_type operator+=(__int_type __i) volatile noexcept;

// -=
__int_type operator-=(__int_type __i) volatile noexcept;

// &=
__int_type operator&=(__int_type __i) volatile noexcept;

// |=
__int_type operator|=(__int_type __i) volatile noexcept;

// ^=
__int_type operator^=(__int_type __i) volatile noexcept;
4、CAS锁
  • 上面介绍了atomic原子类的使用,实际上atomic原子类的底层操作是一个CAS锁进行一个支撑,而CAS是硬件层面保证。

  • CAS锁:CAS的全称是Compare and Swap(比较并交换),经常听到的另外一个名字乐观锁就是这个东西。

  • CAS实现原理:主要设计三个值:旧的预期值A,内存地址值V,新值B。

    • 首先传入旧的预期值A去跟内存中地址值V比较
    • 如果A == V,那么把V用B的值进行替换
    • 如果A != V,那么不做修改,继续重来
      在这里插入图片描述
4.1、ABA问题
  • CAS锁绕不开的一个话题,就是ABA问题:就是如果你有一杯水放在桌子上重100g,先被A倒掉了10g,再被B加入了10g其他物质,最后这杯水重还是10g,但是这杯水已经不能喝了(谁也不知道加了什么),即数据被多次操作,但操作后值还是原来的值

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

  • 解决这个问题可以加入版本号,如果版本号不对就不会进行赋值:可以采用AtomicMarkableReference,AtomicStampedReference进控制

    • AtomicMarkableReference:用于标记这个值是否被修改,修改过就是true,没有修改过就是false,而不管你修改过几次
    • AtomicStampedReference:用于标记这个值修改过多少次,动一次加一次。
4.2、atomic底层CAS源码实现
  • 主要分为weak和strong两大类版本
  • weak版本的CAS允许偶然出乎意料的返回(比如在字段值和期待值一样的时候却返回了false),不过在一些循环算法中,这是可以接受的。通常它比起strong有更高的性能。
bool compare_exchange_weak(_Tp& __e, _Tp __i, memory_order __s, memory_order __f) noexcept{
    return __atomic_compare_exchange(std::__addressof(_M_i),std::__addressof(__e), std::__addressof(__i), true, __s, __f);
}

bool compare_exchange_weak(_Tp& __e, _Tp __i, memory_order __s, memory_order __f) volatile noexcept{
    return __atomic_compare_exchange(std::__addressof(_M_i), std::__addressof(__e), std::__addressof(__i), true, __s, __f);
}

bool compare_exchange_weak(_Tp& __e, _Tp __i, memory_order __m = memory_order_seq_cst) noexcept{
    return compare_exchange_weak(__e, __i, __m, __cmpexch_failure_order(__m));
}

bool compare_exchange_weak(_Tp& __e, _Tp __i, memory_order __m = memory_order_seq_cst) volatile noexcept{
    return compare_exchange_weak(__e, __i, __m, __cmpexch_failure_order(__m));
}

bool compare_exchange_strong(_Tp& __e, _Tp __i, memory_order __s, memory_order __f) noexcept{
    return __atomic_compare_exchange(std::__addressof(_M_i),std::__addressof(__e), std::__addressof(__i), false, __s, __f);
}

bool compare_exchange_strong(_Tp& __e, _Tp __i, memory_order __s, memory_order __f) volatile noexcept{
    return __atomic_compare_exchange(std::__addressof(_M_i), std::__addressof(__e), std::__addressof(__i), false, __s, __f);
}

bool compare_exchange_strong(_Tp& __e, _Tp __i, memory_order __m = memory_order_seq_cst) noexcept{
    return compare_exchange_strong(__e, __i, __m, __cmpexch_failure_order(__m)); 
}

bool compare_exchange_strong(_Tp& __e, _Tp __i, memory_order __m = memory_order_seq_cst) volatile noexcept{
    return compare_exchange_strong(__e, __i, __m, __cmpexch_failure_order(__m)); 
}

相关推荐

  1. C++ 线 atomic

    2024-04-02 12:38:01       62 阅读
  2. C++线编程中的详解

    2024-04-02 12:38:01       24 阅读
  3. C# 线编程:线并发

    2024-04-02 12:38:01       34 阅读
  4. c#线 使用lock

    2024-04-02 12:38:01       36 阅读
  5. C#线之(Thread)详解示例

    2024-04-02 12:38:01       29 阅读
  6. C++】线(二):std::mutex std::atomic的使用

    2024-04-02 12:38:01       59 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-02 12:38:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-02 12:38:01       100 阅读
  3. 在Django里面运行非项目文件

    2024-04-02 12:38:01       82 阅读
  4. Python语言-面向对象

    2024-04-02 12:38:01       91 阅读

热门阅读

  1. c++20 的部分新概念及示例代码-Contracts,Ranges

    2024-04-02 12:38:01       34 阅读
  2. 通过UDP实现参数配置

    2024-04-02 12:38:01       35 阅读
  3. PTA 7-2 数列循环左移 python

    2024-04-02 12:38:01       34 阅读
  4. C++里指针和引用的区别

    2024-04-02 12:38:01       34 阅读
  5. GPT-4智能体:迈向GPT-5的跳板

    2024-04-02 12:38:01       29 阅读
  6. 【机器学习算法介绍】(1)K近邻算法

    2024-04-02 12:38:01       40 阅读
  7. 每天学习一个Linux命令之wget

    2024-04-02 12:38:01       28 阅读
  8. vfox使用指南

    2024-04-02 12:38:01       46 阅读
  9. 01xcxjc

    2024-04-02 12:38:01       35 阅读
  10. 高校智慧教室物联网系统设计与实现的探索

    2024-04-02 12:38:01       27 阅读
  11. PTA 7-18 蛇鸟 python

    2024-04-02 12:38:01       33 阅读