《Effective Modern C++》学习笔记 - Item 40: 对于并发使用std::atomic,对于特殊内存使用volatile

  • std::atomic<T> 类型和 volatile 的功能完全不同,前者应该被用于实现并发的原子操作,而后者应该用于存储在特殊内存中的对象。

  • std::atomic<T> 模板的实例化对象保证对该对象的操作对于所有线程都是原子的,表现如同受 mutex 保护一样(但是实现上一般会使用比 mutex 更底层高效的机器指令),例如:

std::atomic<int> ai(0); 	// initialize ai to 0
ai = 10; 					// atomically set ai to 10
std::cout << ai; 			// atomically read ai's value
++ai; 						// atomically increment ai to 11
--ai; 						// atomically decrement ai to 10

例如其中 ++ai 的操作,假设有两个线程在同时运行这段代码,如果 ai 不是 atomic 类型,那么可能会出现交错执行的问题:

  1. 线程 1 读取 ai 的值,为10;
  2. 线程 2 读取 ai 的值,仍为10;
  3. 线程 1 将读取的值(10)自增为 11,然后写回 ai
  4. 线程 2 也将读取的值(10)自增为 11,然后写回 ai

于是 ai 的值最终为 11。原子类型保证了这种情况不会发生。另外,考虑下面一段代码:

std::atomic<bool> valAvailable(false);
auto imptValue = computeImportantValue(); // compute value
valAvailable = true; // tell other task it's available

显然,computeImportantValue() 函数在设置 valAvailable 之前完成调用从逻辑上是必要的。然而对于编译器来说,两组无关变量的赋值,如:

a = b;
x = y;

可以被重新排序(reorder) 的,编译器可以把以上赋值的顺序调换执行。即使编译器不这么做,底层硬件上为了提高执行效率也可能如此做。使用 std::atomic 类型可以保证编译器不会做如此的重排,而且编译器还会保证生成的机器码也保持源代码的执行顺序。

至此可见 std::atomic 类型对于并发的重要性。以上性质对于 volatile 关键字修饰的变量均不成立。


  • 简单来说,volatile 是告诉编译器它们在面对 表现不正常的内存。“表现正常” 的内存拥有两个性质:
  1. 当你将某个值写入了一个内存位置后,该值将保持不变,直到其它东西覆写了它。因此如果有以下代码:

    int x = 0;
    auto y = x; // read x
    y = x; 		// read x again
    

    那么最后一句 y = x 的赋值可以被消除,因为 x 的值在其间没有改变,两次读取的一定是相同的值。

  2. 当你将某个值写入了一个内存位置后从来没有读取它,然后再次写入该位置,那么第一次写入可以被消除(因为从来没用过)。例如:

    x = 10; 	// write x
    x = 20; 	// write x again
    

    x = 10 可以被消除。

    扫描二维码关注公众号,回复: 14691010 查看本文章

以上两种情况分别被称为 redundant loadsdead stores,是编译器优化时主要考虑的问题之一。虽然我们大概不会写出这样的代码,但是在编译器进行模板实例化,内联和重排优化后就很可能出现。

然而,以上优化能够进行的前提是内存 “表现正常”。“特殊” 的内存,最常见的如 I/O 映射内存,能够与外围设备通讯。例如外围设备是一个传感器,那么重复读就不是多余的,因为这些内存存储的值对于程序来说是可以主动变化的;如果外围设备是一个发射器,那么重复写就不是多余的,因为外围设备可能随时要从这些内存中读取值。

volatile 关键字的作用就是声明这种情况,也就是在告知编译器 不要对这些存储上的对象做任何优化。而 std::atomic 则没有这种限制,其包装的变量依然可以被做多余读写的优化。


  • 至此可以看出,std::atomicvolatile 二者功能上不同。实际上,二者可以被同时使用:
volatile std::atomic<int> vai; 	// operations on vai are atomic
 								// and can't be optimized away

总结

  1. std::atomic 用于多线程数据的操作(无需 mutex),是编写多线程软件的工具。
  2. volatile 用于内存读写不应该被做优化的情况,是应对特殊内存的工具。

猜你喜欢

转载自blog.csdn.net/Altair_alpha/article/details/123935872