Kno*_*abe 11 c++ volatile c++11
考虑以下对volatile
内存的写入顺序,我从David Chisnall在InformIT的文章 "理解C11和C++ 11 Atomics"中获取:
volatile int a = 1;
volatile int b = 2;
a = 3;
Run Code Online (Sandbox Code Playgroud)
我对C++ 98的理解是,根据C++ 98 1.9,这些操作无法重新排序:
符合实现需要模拟(仅)抽象机器的可观察行为,如下所述...抽象机器的可观察行为是它对易失性数据的读写顺序和对库I/O函数的调用
Chisnall说,对订单保存的约束仅适用于单个变量,写出符合要求的实现可以生成执行此操作的代码:
a = 1;
a = 3;
b = 2;
Run Code Online (Sandbox Code Playgroud)
或这个:
b = 2;
a = 1;
a = 3;
Run Code Online (Sandbox Code Playgroud)
C++ 11重复了C++ 98的措辞
符合实现需要模拟(仅)抽象机器的可观察行为,如下所述.
但这说关于volatile
s(1.9/8):
严格根据抽象机器的规则来评估对volatile对象的访问.
1.9/12表示访问volatile
glvalue(包括变量a
,b
及c
以上)是副作用,1.9/14表示一个完整表达式(例如,一个语句)中的副作用必须先于后面的副作用在同一个线程中完整表达.这使我得出结论,Chisnall显示的两个重新排序是无效的,因为它们不符合抽象机器所规定的排序.
我忽略了什么,还是Chisnall错了?
(请注意,这不是一个线程问题.问题是是否允许编译器重新排序对volatile
单个线程中不同变量的访问.)
IMO Chisnalls的解释(由你提出)显然是错误的.更简单的情况是C++ 98.将sequence of reads and writes to volatile data
要保存需求,并适用于有序序列读取和写入任何挥发性的数据,而不是一个变量.
如果你考虑volatile的最初动机:内存映射I/O,这就变得很明显了.在mmio中,您通常在不同的存储器位置有几个相关的寄存器,并且I/O设备的协议需要对其寄存器组进行特定的读写操作序列 - 寄存器之间的顺序很重要.
C++ 11的措辞避免讨论绝对sequence of reads and writes
,因为在多线程环境中,跨线程没有一个明确定义的此类事件序列 - 如果这些访问转到独立的内存位置,这不是问题.但我相信,对于任何具有良好定义顺序的易失性数据访问序列,规则保持与C++ 98相同 - 无论在该序列中访问多少个不同位置,都必须保留顺序.
这是一个完全独立的问题,需要实现.如何(甚至可以)从程序外部观察易失性数据访问以及程序的访问顺序如何映射到外部可观察事件是未指定的.实施应该可以给你一个合理的解释和合理的保证,但合理的取决于上下文.
C++ 11标准为非同步的易失性访问之间的数据竞争留下了空间,因此没有任何东西需要通过完整的内存栅栏或类似的结构来围绕它们.如果存储器的某些部分真正用作外部接口 - 用于存储器映射的I/O或DMA - 则实现可能是合理的,可以保证对这些部件的易失性访问如何暴露给消耗设备.
可以从标准中推断出一种保证(参见[into.execution]):volatile std::sigatomic_t
即使在信号处理程序中,类型的值必须具有与写入顺序兼容的值 - 至少在单线程程序中.
你是对的,他是错的。对不同易失性变量的访问不能由编译器重新排序,只要它们出现在单独的完整表达式中,即由 C++98 所谓的序列点分隔,或者用 C++11 术语来说,一个访问先于另一个访问排序。
Chisnall 似乎试图解释为什么volatile
对于编写线程安全代码来说毫无用处,他展示了一个简单的互斥实现,volatile
该实现依赖于编译器重新排序会破坏的互斥实现。他是对的,这volatile
对于线程安全来说毫无用处,但并不是因为他给出的原因。这并不是因为编译器可能会重新排序对volatile
对象的访问,而是因为 CPU 可能会重新排序它们。原子操作和内存屏障会阻止编译器和CPU 根据线程安全的需要跨屏障重新排序。
请参阅 Sutter 的信息丰富的“挥发性与挥发性”文章中表 1 右下角的单元格。