书中举了一个很简单的例子:
int global_x = 0; // 两个线程共享的全局变量.
Thread1: // 线程 1 的定义 Thread2: // 线程 2 的定义
lock(); lock();
global_x++; global_x++;
unlock(); unlock();
看上去,因为线程 1 和线程 2 在访问 global_x
时都使用了 lock()
和 unlock()
保护,因此 global_x++
的行为不会被并发破坏,所以在线程 1 和线程 2 结束之后,global_x 的值似乎一定是 2。但其实,这么理所当然的猜测有可能是错误的。解释如下:
出现这样的问题,是因为编译器为了提高global_x
的访问速度,将global_x
的值放到了某个寄存器里,这就导致了所谓过度优化的问题。
书中给出的为了阻止过度优化的方法是使用 volatile
关键字。(注:这里的 volatile 仅指 C/C++的关键字,不要和 java 中的搞混)
我的理解是volatile
从来不是多线程中需要的,靠操作系统提供的同步原语应该就足够了。
相关链接:
pthread locks implement memory barriers that will ensure that cache effects are made visible to other threads. You don't need volatile to properly deal with the shared variable i if the accesses to the shared variable are protected by pthread mutexes.
1
wheeler OP |
2
qieqie 2019-12-23 23:45:19 +08:00
原则上编译器应当不需要 volatile 的提示也要保证程序的正确性。但实际上编译器优化基本上是个黑盒的过程,所以有这么个关键字作为一个提示手段。
另外我手边也有这本书,书的写作时间是十年前了,当年主流编译器的行为和现在的也可能存在一定的出入。 |
3
wevsty 2019-12-24 00:59:22 +08:00
就楼主举的这个例子来说,我认为没有 volatile 编译器也会正确的进行优化,不会导致计数结果出现错误。
原因是在这个代码的函数中没有做除了++以外的操作,++操作也要求一定要同步修改到内存,并没有什么必要需要把操作数保存到寄存器里来预先载入。 从汇编的角度来看,编译器转化为 ASM 的伪代码应该类似于: ``` call lock() mov <reg>,<mem> inc <reg> mov <mem>,<reg> call unlock() ``` 甚至更简单一些直接化简为: ``` call lock() inc <mem> call unlock() ``` 为了保持语义正确,这已经是最简化的代码了,没有什么优化的空间。 对于单纯写入内存的操作,操作系统提供的同步语义已经能提供足够的保护了。 但 volatile 也并非在多线程开发中没有意义,举个例子: ``` int global_x = 0; void thread_01() { while(global_x < 100) { sleep(1); } } void thread_02() { lock(); global_x++; unlock(); } ``` 当 thread_01 执行的时候,thread_01 内部并没有对 thread_01 做任何修改,这时候编译器无法预测到 global_x 可能被改变,所以优化的时候很有可能会把 global_x 放到寄存器来加速循环的执行。 这种时候即使 thread_02 里对 global_x 操作的时候加了锁,最终 thread_01 还是可能会陷入死循环。 volatile 关键字代表强制要求编译器每一次使用这个变量的时候都必须从内存读取,所以在这个例子中,使用 volatile 后就可以避免死循环的出现。 所以通常的,对于可能被读取线程以外的什么条件或者代码改动的变量应该使用 volatile 关键字才不容易出现问题。 |
4
secondwtq 2019-12-24 02:12:00 +08:00 1
volatile 是给编译器看的,过了编译器就不需要了。也就是如果楼主直接写汇编是根本不需要 volatile 之类的概念的。
这是条件之一。也是楼主提到的 另一个条件是 SO 回复中的"function call (a sequence point in C)"( C++ 里面是 sequenced-before 关系) ,这使得编译器必须保证生成 #3 的代码,而不是先放到寄存器里面等 unlock 之后再存回内存 如果没有这个保证,书里面的问题是真实存在的,楼主不能只看 OS 不看编译器 |
5
secondwtq 2019-12-24 02:16:17 +08:00
@wevsty 楼主这个明显是个 minimal example,在实际代码里面很多不是”函数中没有做除了++以外的操作“的情况(比如在算法中更新全局的 statistics (嘛,虽然这种情况一般应该用一个 local 变量存下))
如果这个变量的 live range 比较长(在函数中被用了多次),并且没有 function call (或没有 function call 作为 sequence point 的保证),那编译器是可能选择把它在寄存器里面扣一会的 |
6
anytk 2019-12-24 10:03:40 +08:00 1
书中的这个例子恐怕需要商榷,锁语义会插入 memory barrier,进锁会读同步,出锁会写同步。
|