http://haohaoweixiao.blog.sohu.com/161972697.html
http://baiy.cn/doc/cpp/advanced_topic_about_multicore_and_threading.htm#??????????_volatil
volatile 关键字确实与原子操作有预定关联,但他们之间的关系并不像很多人想象的那么单纯:
C/C++ 中的 volatile 关键字提供了以下保证:
|
volatile *不* 提供如下保证
|
而原子操作要求做到以下保证:
|
可见,使用 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 乱序执行优化对内存访问顺序的影响,通常用于保证多个变量交叉访问的逻辑顺序。内存屏障分为以下几种:
全屏障语义适用范围最广,任何使用其它三种操作的场合都可以安全替换为全屏障操作。但是由于最大限度的禁用了处理器的乱序执行能力,全屏障语义也是效率最低的操作。Acquire 操作保证后续内存访问一定在当前操作完毕后进行,主要用于实现互斥量、信号量的上锁操作。Release 保证前导内存访问一定在操作开始前执行完毕。主要用于实现互斥量、信号量的解锁操作。无屏蔽语义完全不禁用 CPU 的乱序执行优化,效率最高,可广泛适用于简单的引用计数等操作。 参考资料:
|
顺带提一下多处理器环境中的临界区内数据可见性问题:
在由互斥量、信号量和条件变量等互斥手段保护的临界区内访问的数据,通常不需要添加 volitale 声明或其它任何特殊操作就可以保证在不同 CPU 间的一致性。这是因为:互斥量等任何可同步对象的上锁和解锁都需要使用非只读的原子量操作来实现。而所有非只读原子量操作都隐含了一个 CPU cache 同步。所以临界区中的数据总是能够保持多 CPU 间的一致性。至于寄存器优化的问题,由于临界区是互斥进入的,所以在临界区内对读操作进行的寄存器优化不会产生一致性问题。而对于写操作,除非错误的使用了同步算法,否则在出临界区前所有可能被其他线程访问的对象一定会被从寄存器写回内存或 cache 中。所谓“可能被其他线程访问的对象”是指所有不在当前线程运行时栈上创建的对象(即:所有非 auto 型对象)。