MESI协议里,状态的转换比较复杂,但是都和人的直觉一致。对于我们研究的问题而言,只需要知道:
当是Shared状态的时,修改Cache Line的内容前,要先通过Request For Ownership (RFO)的方式广播通知其它核,把Cache Line置为Invalid。
当是Modified状态时,Cache控制器会(snoop)拦截其它核对该Cache Line对应的内存地址的访问,在回应回插入当前Cache Line的数据。并把本Cache Line的内容回写到内存里,状态改为Shared。
因此,并不会存在一个核内的Cache数据修改了,另一个核没有感知的情况。
即不会出现线程A修改了Cache中的内容,线程B一直读取到的都是旧数据的情况。考虑到CPU内部通迅都是很快的,本人估计线程A修改了共享变量,线程B读取到新值的时间应该是纳秒级之内。
还有一个坑:CPU乱序执行
现代很多CPU都有乱序执行能力,从上面加了volatile之后生成的汇编代码来看,没有什么特别的地方。那么它对于CPU乱序执行也是无能为力的。比如:
volatile static int flag = -1;
void thread1(){
...
jobA();
flag = 1;
}
void thread2(){
...
while(1){
if(flag > 0)
jobB();
}
}
对于这两个线程,jobB()有可能比jobA()先执行!
因为thread1里,可能会因为CPU乱序执行,先执行了flag = 1,再执行jobA()。
那么如何防止这种情况?这个麻烦是CPU搞出来的,自然也是CPU提供的解决办法。
GCC内置了一些原子内存访问的函数,如:
http://gcc.gnu.org/onlinedocs/gcc-4.6.2/gcc/Atomic-Builtins.html
type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)
这些函数实际即隐含了memory barrier。
比如为之前讨论的代码加上memory barrier:
while(true){
__sync_fetch_and_add(&vvv,0);
if(vvv < 0 )
break;
}再查看下生成的汇编代码:
.L4: lock addl $0, _ZL3vvv(%rip) movl _ZL3vvv(%rip), %eax shrl $31, %eax testb %al, %al je .L5 jmp .L8 .L5: jmp .L4可以看到,加多了一条 lock addl 的指令。
这个lock,实际上是一个指令前缀,它保证了当前操作的Cache Line是处于Exclusive状态,而且保证了指令的顺序性。这个指令有可能是通过锁总线来实现的,但是如果总线已经被锁住了,那么只会消耗后缀指令的时间。
实际上Java里的volatile就是在前面加了一个lock add指令实现的。这个有空再写。
其它的一些东东
有些场景可以不用volatile
抛开上面的讨论,其实有些场景可以不使用volatile,比如这种随机获取资源的代码:
ramdonArray[10];
int pos = 0;
Resource getResource(){
return ramdonArray[pos++%10];
}
这样的代码pos是非volatile,但多线程调用getResource()函数完全没有问题。
C11与C++11
为什么C11和C++11不把volatile升级为java/C#那样的语义?我猜可能是所谓的“兼容性”问题。。蛋疼
C++11提供了Atomic相关的操作,语义和Java里的volatile差不多。但是C11仍然没有什么好的办法,貌似只能用GCC内置函数,或者写一些类似的汇编的宏了。
http://en.cppreference.com/w/cpp/atomic
GCC优化的一些东东
其实在讨论的代码里,如果while循环里多一些代码,GCC可能就分辨不出是否能优化了
优化的一些东东:
比如,在大部分语言里(特别是动态语言),第一份代码要比第二份代码要高效得多。
//1
int len = array.length;
for(int i = 0; i < len; ++i){
}
//2
for(int i = 0; i < array.length; ++i){
}
总结:
回到最初的问题:多线程共享非volatile变量,会不会可能导致线程while死循环?
其实这事要看很多别的东西的脸色。。编绎器的,CPU的,语言规范的。。
对于没有被编绎器优化掉的代码,CPU的Cache一致性协议(典型MESI)保证了,不会出现死循环的情况。这个不是volatile的功劳,这个只是CPU内部的正常机制而已。
对于多线程同步程序,要小心地在合适的地方加上内存屏障(memory barrier)。
参考:
http://en.wikipedia.org/wiki/Volatile_variable
http://en.wikipedia.org/wiki/MESI
http://en.wikipedia.org/wiki/Write-back#WRITE-BACK
http://en.wikipedia.org/wiki/Bus_snooping
http://en.wikipedia.org/wiki/CPU_cache#Multi-level_caches
http://blog.jobbole.com/36263/ 每个程序员都应该了解的 CPU 高速缓存
http://stackoverflow.com/questions/4232660/which-is-a-better-write-barrier-on-x86-lockaddl-or-xchgl
http://stackoverflow.com/questions/8891067/what-does-the-lock-instruction-mean-in-x86-assembly
http://gcc.gnu.org/onlinedocs/gcc-4.6.2/gcc/Atomic-Builtins.html
http://en.c