处理器协同机制其三C++内存顺序与栅栏(及依赖性读屏障)

3 篇文章 0 订阅
3 篇文章 0 订阅

 目录

处理器协同机制其一缓存一致性协议(MESI)

处理器协同机制其二内存屏障与内存顺序(及Store Buffer与Invalidate Queue)

处理器协同机制其三C++内存顺序与栅栏(及依赖性读屏障)

读写屏障的硬件原理:

取取屏障,保证其前加载先于其後。处理器对此不乱序,且加载的结果有序,是为取取屏障。

存存屏障,保证其前存储先于其後。处理器对此不乱序,且存储的结果有序,是为存存屏障。

取存屏障,保证其前加载先于其後存储。但需处理器对此不乱序,即为取存屏障。

存取屏障,保证其前存储先于其後加载。处理器对此不乱序,且当即处理所有存储缓冲(之後的加载可能即时發生,此前对缓存的更改必须完成),此前无效队列的消息必须在此後相关缓存行的加载之前处理完成(保证之後加载的数据有效)。

而处理器实现有三种内存屏障:

读屏障,处理器对此不乱序,且标记(或清空)无效队列(或许无需此操作,因为本来就是按照标记之後的方式处理无效队列)。是实现取取屏障语义。通常,亦实现取存屏障语义。

写屏障,处理器对此不乱序,且标记(或清空)存储缓冲。是实现存存屏障语义。

完全屏障,处理器数据读写不乱序,且清空存储缓冲(此操作有异于写屏障)和标记(或清空)无效队列(对无效队列的操作同读屏障)。是实现存取屏障语义。

依赖性读屏障与读屏障的区别:

       逻辑上,有依赖性的加载操作不能重排序(乱序执行原则一),即从当前CPU来看,其加载是顺序的,如果从加载的结果来看是乱序的,那么一定出现了缓存不一致的情况。也就是说处理器系统能保证整体的缓存一致性,那么就不存在依赖性加载操作乱序的问题。

       简言之,另一处理器依序写入其缓存,某处理器依序从其缓存里面读取,出现了对缓存的修改的感知顺序不一致,这就需要依赖性读屏障保证一致性。依赖性读屏障实现可以是,标记了当前无效队列,此後若无效队列中有相关标记的项则该缓存行的加载必须等待之後进行。而读屏障不仅包含依赖性读屏障的语义,更加保证了加载操作不乱序执行。

Alpha在Linux内核中的依赖性读屏障:

在Alpha架构,smp_read_barrier_depends()被定义为mb指令。虽然依赖性读屏障仅需rmb指令就包含其语义,但使用过程中,插入依赖性读屏障的位置可能也需要写屏障(甚至是完全屏障),如下:

a+=i;//即使可以做到原子操作

smp_read_barrier_depends();

b=*a;

       在高级语言中,加载操作通常也伴随着存储操作,以上代码如果只用一个依赖性读屏障来保证多处理器系统上的顺序性,那么必须附以额外的语义,从汇编代码来考虑:

ldl [a],%1 ;操作数顺序按AT&T汇编

addl %1,#i,%0

stl %0,[a]

mb ;smp_read_barrier_depends()

ldl [%0],%1

stl %1,[b]

       很明显,如果“a+=i”不是原子操作,没有屏障则执行读写可以乱序,应该插入完全屏障。

       如果“a+=i”是原子操作,理应只需要读屏障即可,然而Alpha处理器的原子性操作不能得到保证,之後的加载和存储操作都可以提前到原子操作完成前处理,只用读屏障无法避免,因此也应使用完全屏障。

       可以知道,依赖性读屏障定义为完全屏障虽然避免了错误使用的後果,但是在只需要读屏障的情况下,这是有点浪费的。新的Linux内核已经不推荐smp_read_barrier_depends(),而在不同情况使用不同的屏障(smp_rmb, smp_wmb, smp_mb),通常的依赖性读屏障语义应使用smp_rmb(),而包含其他语义使用smp_wmb或smp_mb(这在其他平台也可能是需要的),且增加了READ_ONCE和WRITE_ONCE宏取代smp_read_barrier_depends()的作用。

内存顺序模型:

       所有处理器架构都可以分为以下几个内存顺序模型:强一致性、访存一致性、弱一致性。

       实际上,有关内存顺序有两方面,执行顺序性和访存一致性。执行顺序性是指处理器遵守处理顺序,访存一致性是指处理器从其缓存访问数据的感知顺序是一致的。

       强一致性模型满足执行顺序性和访存一致性,其通常不需要内存屏障,仅仅可能需要(完全屏障,存取屏障的语义要求执行顺序性和访存一致性同时满足)。

访存一致性模型只满足访存一致性,并在一定程度上满足执行顺序性,不需要依赖性读屏障(乱序执行原则一),其他类型屏障通常是需要的。

弱一致性模型既不满足访存一致性也不满足执行顺序性,但在一定程度上满足(乱序执行原则),通常需要几乎所有类型的内存屏障(Alpha架构就是如此)。

C++内存顺序:

       C++内存顺序由<atomic>头文件定义,用于控制原子操作的内存顺序。有六种:

1memory_order_relaxed

       宽松的,只保证此操作的原子性,没有内存屏障等。有此顺序的内存栅栏通常无意义,某些编译器实现为“编译器屏障”(这不是c++语义)。

2、memory_order_release

       释放语义,此原子操作之前的读写不能重排至其後,其他线程获得此原子操作的存储结果也必定可见其之前的写入。

       释放语义施加于存储操作,其在此存储之前插入一个内存屏障(包含了“存存屏障”和“取存屏障”的语义),通常是写屏障。注意,原子操作本身可能包含读写,其是不可拆分的

3、memory_order_acquire

       获得语义,此原子操作之後的读写不能重排至其前,其获得其他线程释放的同一原子变量也必定可见释放之前的写入。

       获得语义施加于加载操作,其在此加载之後插入一个内存屏障(包含了“取取屏障”和“取存屏障”的语义),通常是读屏障

4、memory_order_consume

       消费语义,之後依赖于此原子操作的读写不能重排至其前,其消费(获得)其他线程释放的同一原子变量也必定可见释放之前的依赖于此原子变量的数据的写入。

       消费语义施加于加载操作,其在此加载之後插入一个内存屏障,通常是依赖性读屏障

       然而,可能大部分编译器的实现等同于memory_order_acquire(避免错误使用的後果)。

5、memory_order_acq_rel

       获得释放语义,获得和释放语义同时具有,此原子操作与其他读写不重排。

       在原子操作(读改写)施加获得释放语义就具有完全屏障的效果。而C++不保证有此顺序的内存栅栏能有“存取屏障”的语义,然而可能实现以完全屏障

6、memory_order_seq_cst

       序列一致顺序,含有获得、释放语义,此外,加上一个全局顺序,所有线程观测到的此类原子操作的發生顺序都是一致的。

       此类原子操作通常使用带lock前缀的指令实现(x86)。序列一致顺序的C++内存栅栏具有完整的内存屏障的语义,实现即完全屏障

C++内存栅栏的函数定义:

extern "C" void atomic_thread_fence(std::memory_order order) noexcept;

C++编译栅栏的函数定义:

extern "C" void atomic_signal_fence(std::memory_order order) noexcept;

       C++中提供了两种栅栏,内存栅栏(atomic_thread_fence)用于解决线程之间表现的内存乱序,其阻止CPU指令层面的乱序优化;编译栅栏(atomic_signal_fence)用于解决与信号处理程序(一种软中断程序)之间表现的内存乱序,其仅阻止编译器层面的乱序优化,不产生内存屏障指令。

C++ volatile与atomic的作用:

       volatile(易变的)修饰的变量,其访问必须与存储交互,不能仅用寄存器进行,这表示此类变量不会被优化掉,能确保在内存中占据一个位置,而且volatile变量之间及I/O访问的顺序不能改变,即volatile变量的访问和I/O一样保证顺序。其有以下含义:

1、不会在两个操作之间把volatile变量暂存在寄存器中。

2、不做常量合并、常量传播等优化。

3、不会改变volatile变量之间及I/O的访问顺序。

       atomic变量,保证对其的某个操作是原子的(不可分割)。用于多线程,可得到正确的结果,而仅使用volatile的结果通常是不可预测的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值