我相信你一定遇到这样的问题:你把某人的名字和电话号码写到你的通讯录中,数个月之后企图打电话给这个人,却发现资料已经过期了。同样的情况也可能发生在编译器为你产生的程序代码中。
编译器最优化的结果是,设法把常用的数据放在CPU的内部寄存器中,这些寄存器就像你的通讯录一样,数据从寄存器中读出,远比从内存中读出快得多,就好像使你从你的通讯录中读数据,远比从大电话簿中读数据要快得多,当然,如果另一个线程改变了内存的变量值,那么此变量在寄存器中的拷贝值就算是“过期”了。
在一个单线程中,这种情况不可能发生,编译器可以分析你的程序的每一个操作,然后确保数据在适当时候会被重新载入。然而在一个多线程中就不可能知道其他线程在做什么,所以编译器一定不能够允许让一个共享变量的值拷贝到寄存器中。
C和C++有一个鲜为人知的关键字,教导编译器如何在一个variable-by-variable 的基础上采取行动,这个关键字是volatile。这个关键字告诉编译器不要持有变量的临时性拷贝,它可以适用于基础类型,如int或long,也适用于一整个C结构或C++类。后面这种情况下,结构或类的所有成员都会被视为volatile。
使用volatile并不会否定critical sections或mutexes的需要。
例如你说:
a = a + 3
还是会有一小段时间,a会被放在一个寄存器中,因为算数运算只能够在寄存器中进行。一般而言,volatile关键字适用于行与行之间,而不是放在行内。
让我们看一个非常简单的函数,观察编译器制造出来的汇编语言中的瑕疵,并看看volatile如何修正这个瑕疵。这个范例函数就是一个busy loop
,虽然不建议写busy loop,但是它却是解释这里的观念的一个最好的例子。本例之中,WaitForKey()等待一个字符的到来:
void WaitForKey(char* pch)
{
while ( *pch == 0 )
;
}
当你把最优化选项都关闭后,编译这个程序,获得以下结果,进入点和退出点已经被我移除(为了让程序代码清爽一些),粗体字代表C源代码。
; while(*pch == 0)
$L27:
; Load the address stored in pch
mov eax, DWORD PTR _pch$[ebp]
; Load the character into the EAX register
movsx eax,BYTE PTR _pch$[eax]
;Compare the value to zero
test eax, eax
; If not zero, exit loop
jne $L28
; ;
jmp $L27
$L28:
; }
这个未曾优化的函数代码不断地载入适当的地址,载入地址中的内容,测试其结果,慢,但是准确,此版本在多线程程序上没有问题。
现在我们看看最优化带来什么影响:
; {
; Load the address stored in pch
mov eax, DWORD PTR _pch$[esp - 4]
; Load the character into the AL register
movsx al, BYTE PTR [eax]
; while (*pch == 0)
$L84:
; Compare the value in the AL register to zero
test al, al
; If still zero, try again
je SHORT $L84
; ;
; }
短多了,最优化果然有用。但是请注意,编译器把MOV指令放到循环之外,这个操作称为 loop-invariant removal。这个在单线程程序中应该是一个很好地最优化,但是在多线程程序中,如果另一个线程改变了数值,则循环永远不会结束。被测试的值永远被放在寄存器中,很明显那是一只“臭虫”。
解决方法就是重写WaitForKey(),把参数pch声明为 volatile:
void WaitForKey (volatile char* pch)
{
while (* pch == 0 )
;
}
这项改变对于非最优化的版本没有影响,但请看看最优化后的结果:
; {
; Load the address stored in pch
mov eax, DWORD PTR _pch$[esp-4 ]
; while (*pch == 0)
$L84:
; Directly compare the value to zero
cmp BYTE PTR [eax], 0
; If still zero, try again
je SHORT $L84
; ;
; }
这个版本几乎完美,地址不会改变,所以地址声明被移到循环之外,地址内容是volatile,所以每次循环之中它不断地被重新检查。
精细地说,把一个const volatile 变量传给函数作为参数是合法的,如此的声明意味着函数不能够改变变量的值,但是变量的值却可以被另一个线程在任何时间改变掉。
const 和 volatile 都是ANSI的标准关键字,所有的C/C++编译器都应该有支持。