首页 > Note > 从单例模式谈起(二):volatile 关键字

从单例模式谈起(二):volatile 关键字

2020年1月21日 发表评论 阅读评论

前一部分(从单例模式谈起(一))我们讨论单例模式时,谈到了 Double-Checked Locking 问题。我们讲, volatile 关键字并不能解决Double-Checked Locking 问题。这一节我们讨论与此相关的问题。
现代编译器为了提高程序的效率而对代码做了很多优化。这些优化大部分是有益的,但是也为多线程编程带来问题。

寄存器缓存

我们来看如下代码:

x++ 的值有锁保护,所以它是独占的,x++ 的行为不会被并发破坏。那么 x 的值必然为 2。 然而现实中并非一定如此:编译器有可能为了提高 x 的访问速度而将 x 放入线程的某个寄存器中,而线程的寄存器是线程私有的。 如果 Thread1 先获得了锁,那么程序的执行过程有可能是这样的:

  1. [Thread1] 读取 x 到寄存器 R1. (R1 = x = 0)
  2. [Thread1] R1++. unlock. 由于之后可能还会访问 x, 所以 Thread1 不将 R1 写回 x . (x = 0)
  3. [Thread2] 读取 x 到寄存器 R2 (R2 = x = 0)
  4. [Thread2] R2++
  5. [Thread2] 将 R2 写回 x (x = R2 =1)
  6. [Thread1] 在某个时候将 R1 写回 x (x = R1 = 1)

由此可见,即使正确的加了锁,也不能保证多线程安全。

指令顺序调整

再看一个例子:

理论上, r1, r2 中至少有一个为1, 而不可能同时为0。但实际中 r1=r2=0 的情况时有发生。这是因为编译器发殿出了 动态调度 , 为了提高效率有可能交换指令的顺序。也就是说,上面的代码可能是按下面的顺序来执行的:

而我们前一节提到的 DoubleChecking 问题,也是由此产生的。

volatile

volatile的引入是为了阻止过度优化,它可以做两件事:

  1. 阻止编译器为了提高效率而将 volatile 变量缓存到寄存器中却不写回
  2. 阻止编译器调整 volatile 变量的指令顺序

volatile 可以解决第一个问题,但无法解决第二个问题。因为除了编译器会动态调整指令顺序, CPU 也具有动态调整指令顺序的能力。而在 C++ 层面来阻止 CPU 动态调整指令是一件很困难的事。
很多CPU 会提供一条 barrier 指令,可以用来阻止CPU动态调整指令。但它不在我们今天讨论的范围之内。

总之单从编译器层面来说, volatile 可以在一定程序上保证线程安全,但是在程序的整个运行环境中,volatile 并不是万能的。

 


参考<程序员的自我修养>

  1. 本文目前尚无任何评论.
  1. 本文目前尚无任何 trackbacks 和 pingbacks.