volatile-内存屏障-互斥锁等-----非常好!

http://haohaoweixiao.blog.sohu.com/161972697.html

http://baiy.cn/doc/cpp/advanced_topic_about_multicore_and_threading.htm#??????????_volatil

   

 volatile 关键字确实与原子操作有预定关联,但他们之间的关系并不像很多人想象的那么单纯:

C/C++ 中的 volatile 关键字提供了以下保证:
  1. 对声明为 volatile 的变量进行的任何操作都不会被优化器去除,即使它看起来没有意义(例如:连续多次对某个变量赋相同的值),因为它可能被某个在编译时未知的外部设备或线程访问。
  2. 被声明为 volatile 的变量不会被编译器优化到寄存器中,每次读写操作都保证在内存(详见下文)中完成。
  3. 在不同表达式内的多个 volatile 变量间的操作顺序不会被优化器调换(即:编译器保证多个 volatile 变量在 sequence point 之间的访问顺序不会被优化和调整)。
volatile *不* 提供如下保证
  1. volatile 声明不保证读写和运算操作的原子性。
  2. volatile 声明不保证对其进行的读写操作直接发生在主内存。相反,CPU 会尽可能让这些读写操作发生在 L1/L2 等 cache 上。除非:
    1. 发生了一个未命中的读请求。
    2. 所有级别的 cache 均已被配置为通过式写(write through)。
    3. 目标地址为 non-cacheable 区(主要是其它设备映射到内存地址空间的通信接口。例如:网卡的板载缓冲区、显卡板载显存、WatchDog 寄存器等等)。
  3. 编译器仅保证在生成目标码时不调整 volatile 变量的访问顺序,但通常并不保证该变量不受处理器的 out-of-order 特性影响。目前唯一一个已知的特例是安腾(IA64)处理器版的 VC:在生成 IA64 Target 时,VC 会自动在所有 volatile 访问之间添加内存屏障(详见下文)以保证访问顺序。但 ISO 标准并未要求编译器实现类似机制。实际上,其它编译器(或是面向其它平台的 VC)也都没有类似保证。也就是说,通常认为 volatile 并不保证代码在处理器上的执行顺序,如果需要类似的保证,程序员应当自己使用内存屏障操作。

而原子操作要求做到以下保证:
  • 对原子量的 '读出-计算-写入' 操作序列是原子的,在以上动作完成之前,任何其它处理器和线程均无法访问该原子量。
  • 原子操作必须保证缓存一致性。即:在多处理器环境中,原子操作不仅要把计算结果同步到主存,还要以原子的语义将他们同步到当前平台上其他 CPU 的 cache 中。在大部分硬件平台上,cache 同步通常由锁总线指令完成。意即:逻辑上,所有原子操作都显式使用或隐含使用了一个锁总线操作。例如:x86 指令 'cmpxchg' 中就隐含了锁总线操作。

可见,使用 volitale 关键字并不足以保证操作的原子语义。volitale 关键字的主要设计目的是支持 C/C++ 程序与内存映射设备间的通信。但这并不是说 volitale 关键字对原子操作没有任何帮助:

  • 对于一个被声明为 volitale 类型的原子量来说,如果所有写操作都是原子的,并且完成了必要的 cache 同步,那么读取该原子量时就不必再次对总线上锁。

    这是因为 volitale 关键字保证了对该变量的读操作起码是在当前 CPU cache 中完成的(即:该变量不会被优化到寄存器中)。与此同时,对该变量的所有写操作都已保证原子地完成了所有 CPU 间的 cache 同步以及主存同步操作。所以读操作不管在主存还是当前系统中任意一个 CPU 的 cache 上发生,读到的都是一致的数据。

    在这里,volitale 关键字配合原子量的写入操作一起实现了一个典型的读者/写者同步模型:所有写操作和 cache 同步都保证被原子、互斥地完成;读操作则可以被并发地实现。这也是 Windows API 中没有提供类似 'AtomicLoad' 式语义操作的原因

    当然,这里还有一个隐含的附加条件,就是 CPU 必须能够在一个操作中读入这个原子量。这个条件通常可以忽略,因为超出 CPU 位宽的数据类型通常无法被实现成原子量类型。

内存屏障和 Acquire、Release 语义
内存屏障(Memory barrier, membar) 用于消除 CPU 乱序执行优化对内存访问顺序的影响,通常用于保证多个变量交叉访问的逻辑顺序。内存屏障分为以下几种:
  • 全屏障语义:全屏障逻辑上的操作序列为:'操作&双向同步',即:所有在该操作执行前的内存访问指令不得乱序调换到该指令后执行;所有在该操作执行后的内存访问指令也不得被乱序调换到该指令完成前执行。全屏障语义存在两种变体:读屏障和写屏障。这两种变体都是双向屏障,只不过读屏障仅限制 load 操作的执行顺序,而对于 store 操作的顺序则不做任何限制。相反,写屏障则仅限制 store 操作的执行顺序。
     
  • Acquire 语义:Acquire 逻辑上的操作序列为 '操作-向后同步'。Acquire 操作要求所有后续内存访问都不得被乱序调换到该操作前执行。Acquire 操作经常被用于实现互斥量上锁,通常 Acquire 之后的区域为临界区,向后的方向性同步保证了临界区内的内存访问操作不得提前到 Acquire 之前进行,但不限制 Acquire 之前的内存访问操作被乱序推迟到临界区内执行。
     
  • Release 语义:Release 逻辑上的操作序列为 '向前同步-操作'。Release 操作要求所有前导内存访问都不得被乱序调换到该操作后执行。Release 操作经常被用于实现互斥量解锁,通常 Release 之前的区域为临界区,向前的方向性同步保证了临街区内的内存访问操作不得推迟到 Release 之后进行,但不限制 Release 之后的内存访问操作被乱序提前到临界区内执行。
     
  • 无屏障语义:不为操作增加内存屏障,逻辑上的操作序列为:'操作'。

全屏障语义适用范围最广,任何使用其它三种操作的场合都可以安全替换为全屏障操作。但是由于最大限度的禁用了处理器的乱序执行能力,全屏障语义也是效率最低的操作。Acquire 操作保证后续内存访问一定在当前操作完毕后进行,主要用于实现互斥量、信号量的上锁操作。Release 保证前导内存访问一定在操作开始前执行完毕。主要用于实现互斥量、信号量的解锁操作。无屏蔽语义完全不禁用 CPU 的乱序执行优化,效率最高,可广泛适用于简单的引用计数等操作。

参考资料:

顺带提一下多处理器环境中的临界区内数据可见性问题:

在由互斥量、信号量和条件变量等互斥手段保护的临界区内访问的数据,通常不需要添加 volitale 声明或其它任何特殊操作就可以保证在不同 CPU 间的一致性。这是因为:互斥量等任何可同步对象的上锁和解锁都需要使用非只读的原子量操作来实现。而所有非只读原子量操作都隐含了一个 CPU cache 同步。所以临界区中的数据总是能够保持多 CPU 间的一致性。

至于寄存器优化的问题,由于临界区是互斥进入的,所以在临界区内对读操作进行的寄存器优化不会产生一致性问题。而对于写操作,除非错误的使用了同步算法,否则在出临界区前所有可能被其他线程访问的对象一定会被从寄存器写回内存或 cache 中。所谓“可能被其他线程访问的对象”是指所有在当前线程运行时栈上创建的对象(即:所有非 auto 型对象)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值