《Effective Modern C++》学习笔记之条款四十:对并发使用std::atomic,对特种内存使用volatile_std::atomic和std::volitae-CSDN博客
现在事情应该明确了:
std::atomic对于并发程序设计有用,但不能用于访问特种内存。
volatile对于访问特种内存有用,但不能用于并发程序设计。
volatile std::atomic<int> val; //针对val的操作是原子的,
//并且不可以被优化掉
我们知道,std::atomic模板的实例(例如,std::atomic<int>、std::atomic<bool>和std::atomic<Widget* >等)提供的操作可以保证被其他线程视为原子的。一旦构造了一个std::atomic型别对象,针对它的操作就好像这些操作处于受互斥量保护的临界区域一样 ,但是实际上这些操作通常会使用特殊的机器指令来实现,这些指令比使用互斥量来的更加高效。
考虑以下应用了std::atomic的代码:
std::atomic<int> ai(0); //将ai初始化为0
ai = 10; //将ai原子地设置为10
std::cout << ai; //原子地读取ai的值
++ai; //原子地将ai自增为11
--ai; //原子地将ai自减为10
因此,将valAvailable声明为std::atomic型别可以确保我们的关键顺序需求得到保证,impValue必须被所有线程看到,它是以不晚于valAvailable的时序被更改。
std::atomic<bool> valAvailable(false);
auto impValue = computeImportValue(); //计算值
valAvailable = true; //通知其他任务值已可用
//volatile不能保证顺序
volatile bool valAvailable(false);
auto impValue = computeImportValue(); //计算值
valAvailable = true; //其他线程可能将这个赋值操作视作
//在impValue之前!
在这里,编译器可能会将赋值顺序反转为后impValue
先valAvailable
,即使它不这么做, 也可能不会生成及其代码阻止底层硬件使其他内核上的代码看到valAvailable
在impValue
之前发生改变。
这两个那问题(无法保证操作的原子性,无法对代码重新排序施加限制)解释了为何volatile
对并发编程没用,但是并未解释它在什么情况下有用。简而言之,它的用处就是告诉编译器,正在处理的内存不具备常规行为。
特种内存
此类优化仅在内存行为符合常规时才合法。“特种”内存就是另一回事。
可能最常见的特种内存是用于内存映射IO的内存。这种内存的位置实际上是用于与外部设备(例如,外部传感器、显示器、打印机和网络端口等)通信,而非用于读取或写入常规内存(即RAM)。
而volatile的用处就是告诉编译器,正在处理的是特种内存。它的意思是通知编译器“不要对在此内存上的操作做任何优化”。所以,如果x对应于特种内存,则它应该加上volatile声明饰词:
由于移动操作没有在std::atomic中显示声明,因此,根据Item 17中描述的编译器生成特种函数的规则,std::atomic既不提供移动构造,也不提供移动赋值运算符。)
从x中取值并置入y是可以实现的,但是要求使用std::atomic的成员函数load和store。load成员函数以原子方式读取std::atomic型别对象的值,而store成员函数以原子方式写入之。如果想先用x初始化y,然后将x的值置入y,代码必须如下撰写:
std::atomic<int> y(x.load()); //读取x
y.store(x.load()); //x再次读取
register = x.load(); //将x读入寄存器
std::atomic<int> y(register); //以寄存器值初始化y
y.store(register); //将寄存器值存储入y