C++11及上的原子操作底层原理与锁实现

原子变量与原子操作

基础概念

原子变量:一些基础的数据类型或指针加上原子组件。原子变量具有原子性。

原子操作:对原子变量进行的操作就是原子操作。

原子变量的原子性:对原子变量的操作进入到汇编后会对应好几个步骤,其他线程要么看到这些步骤都没开始,要么看到这些步骤都结束了。

为什么要使用原子变量?

多线程环境下,确保对共享变量的操作不会被干扰,从而避免竞态条件。如果是没有具有原子性的普通变量被多个线程去操作会如何?

举个例子,有个全局变量i初始值是0,线程A对它进行++操作,在汇编层面进行到一半,i还是0.。这时候线程B又对i进行了++操作,进行到一半,线程A执行结束,i是1了,但是后续B完成的操作,是在i为0的基础上进行的,i++后依然被更新为1。

原子操作的重点问题

原子性与内存序问题

原子性

原子性指的是一段指令即使映射到底层,在其他核心看来,要么全部没有没有开始,要么全部结束了,不会让让其他核心看到执行的一个中间状态。

单核处理器实现原子性

一个CPU就是一个处理器,一个处理器可能对应多核,一核可以执行一线程

单核处理器如何实现原子性?只要保证操作指令不被中断即可。这其中涉及到两个关键:1、使用底层自旋锁锁住操作(底层也没有其他锁)。2、屏蔽中断。用于保障其他设备不会抢夺cpu,以及避免加锁失败。这些都是由操作系统完成的,基本不需要用户专门操作。

多核处理器实现原子性

现代的计算机基本都是多核的。在单核处理器实现原子性的基础上,还需要:其他避免其他核心操作相关的内存空间。

曾经是通过“锁总线”的操作实现的,当一个核心使用内存的时候,锁住其他核心也禁止访问内存。这种方法的弊端是其他核心连同非相关的内存空间也无法使用。

现在是用lock操作指令只阻止其他核心访问相关的内存区域。

存储体系介绍

71417472e29644549eb42c4a2d09bd53.png

可以看出L1和L2是每个核心都会有的。L3是一个处理器下的核心共有的。

主存(常规语境下所指的内存)是所有核心共有的。

越往下容量越大,同时读取速度越慢。

Cpu访问缓存的时候会有一个最小的读取单位,叫做cache line,一般是64个字节。

d79c60bdd72a42aaaf39a0b5ed11164c.png

Flag标识的是当前的缓存是否可用。

Tag标识的是当前的缓存是否命中。

Data是缓存数据。

为什么要有cache line?

为了解决cpu运算速度与内存访问速度不匹配的问题。

写回策略

这个策略一般不由用户来实现,而是计算机内部管理缓存和内存存储的策略,目的是实现高性能的数据存储。

写直达策略:每次写操作会写到缓存中,也会写到内存中。写性能会很低,现代计算机很少使用了。

写回策略:尽量把数据存储到缓存之中,如果能写到缓存之中就避免写道内存中。

ba7f693dee6e42a7a962186ded3c73e1.png

是否命中缓存:是指之前的缓存中是否存有这个变量。

脏数据:缓存与内存不一致,或缓存有内存无的数据被标记为脏数据。

写数据的时候如果没有命中缓存,就利用LRU策略寻找到缓存中使用最少的区域,如果这块区域有数据并且是脏数据,那么将这块区域的数据刷入内存中。如果不是直接写入缓存。

读数据的时候如果没有命中缓存,先把数据从内存读入缓存,cpu再进行使用数据。

缓存一致性的问题

缓存不一致问题是核心间共享的变量(比如i),在不同的核心的缓存里内容不一样的问题。这是由于写回策略和cpu的多核属性导致的。

解决缓存不一致的方法。通过总线嗅探机制,如果发生了修改那么该核心将修改事件和数据发布给其他核心,其他核心监听到事件发生之后做出相应的修改和操作。

但是由此又引发了一个问题,如果多个核心同时修改了数据,其他核心该怎么办?这时候需要用事务串行化解决。事务串行化,让修改事件按时间的先后顺序发生。该怎么实现呢?通过锁和lock指令所住相应的内存区域。

然而核心对其他核心里的数据不一定感兴趣,如果每次发声数据修改事件都进行监听或发布,那么总线带宽压力将会很大。

该问题通过实现MESI一致性协议解决。MESI对应着缓存里的数据的四种状态,记录在cache line的flag之中。

bba69a5bb88a462186c0ed92735c73b9.png

内存序

C++中原子操作往往有一个参数是规定内存序,用于指导编译器进行优化,用于指导cpu进行指令重排。可以解决两个问题,一是变量更新是否能马上被其他线程看到,二是代码顺序性的问题。

那么问题来了,为什么会有内存序问题?

因为编译器和cpu会在判断代码顺序不影响程序的情况下,有可能会重排相邻的代码执行顺序。比如一个线程锁住了一块内存空间,另一个线程无法操作那块内存空间,这项操作之后的操作并不刚需这块内存空间,这时候可能会重排先往后执行。这是为了提升整个系统的性能,但有时我们要求一定要按某个顺序执行,那么就有时候就不允许重排操作。

C++11原子变量内存序的相关参数

memory_order_relaxed:松散内存序,只用来保证对原子对象的操作是原子的,在不需要保证顺序时使用;

42200e2ad5c64159a746c0a89264ba2c.png

memory_order_release:释放操作,在写入某原子对象时,当前线程的任何前面的读写操作都不允许重排到这个操作的后面去,并且当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见(即通过MSCI在数据更新的时候立即通知到其他核心);与memory_order_acquire 或memory_order_consume 配对使用;

"当前线程的所有内存写入":指的是当前线程对共享内存的写操作,可能包括变量的赋值、修改等。

"对同一个原子对象进行获取的其他线程":表示其他线程通过获取(读取)相同的原子对象来访问共享内存。

"可见":表示当前线程的内存写入对其他线程是可见的,即其他线程在获取原子对象后能够看到当前线程所进行的内存写入操作。

708c77483e0b45d1aa731e025339f2da.png

memory_order_acquire:获得操作,在读取某原子对象时,当前线程的任何后面的读写操作都不允许重排到这个操作的前面去,并且其他线程在对同一个原子对象释放之前的所有内存写入都在当前线程可见(即该核心能随时看到其他核心对该原子变量的操作);

cd6cc219a6a04dd4a5e21c9c96217a43.png

memory_order_acq_rel:获得释放操作,一个读‐修改‐写操作同时具有获得语义和释放语义,即它前后的任何读写操作都不允许重排,并且其他线程在对同一个原子对象释放之前的所有内存写入都在当前线程可见,当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见;
memory_order_seq_cst:顺序一致性语义,对于读操作相当于获得,对于写操作相当于释放,对于读‐修改‐写操作相当于获得释放,是所有原子操作的默认内存序,并且会对所有使用此模型的原子操作建立一个全局顺序,保证了多个原子变量的操作在所有线程里观察到的操作顺序相同,当然它是最慢的同步模型。

互斥锁的底层实现

d61441876978476db6c976584c0d69f2.png

1、记录使用锁的线程的ID,以便线程可能来回切换。

2、利用阻塞队列记录想获取临界资源却不得的线程,等锁释放之后依次获取临界资源。

3、屏蔽中断,即防止其他核心或硬件中断被锁住的区域的代码的执行,令锁住的区域的变量具有原子性。以及如果被中断可能会造成加锁失败。

4、原子操作,即在底层是硬件在用自旋锁实现的。

  • 31
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值