本文的copyleft归gfree.wind@gmail.com所有,使用GPL发布,可以*拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接,严禁用于任何商业用途。
======================================================================================================
关于volatile的说明,这是一个老生常谈的问题。volatile的定义很简单,将其理解为易变的,防止编译器对其进行优化。那么其用途一般有以下三种:
1. 外部设备寄存器映射后的内存——因为外部设备寄存器可能会由于外部设备的状态变化而改变,因此映射后的内存需要注明为volatile的;
2. 多线程或异步访问的全局变量;
3. 嵌入式汇编——防止编译器对其优化;
这三种用途中的第一种和第三种一般都没有什么疑问。但是关于第二个用途,经常有朋友对其有误解。
首先说一下,什么时候情况下全局变量需要有volatile修饰呢?比如,我们大部分的全局变量都是用锁保护的,那么是否还需要volatile呢?答案是否定的,即当全局变量由锁保护的时候,该全局变量是不需要volatile的。原因有二:一是锁保证了临界区的串行;二是锁的实现中有内存屏障,保证临界区访问全局变量为最新值。那么,就可以总结出,当没有锁保护的全局变量是需要使用volatile修饰的,如以下这种情况:
1. 全局变量int exit_flag = 0;
2. 线程1的主循环的退出条件是检查exit_flag是否为1,为1,则退出主循环;
3. 线程2在某些情况下,修改exit_flag为1。
另外如果是异步情况,即没有线程2,而有一个信号处理函数,在收到指定信号的情况下将exit_flag赋值为1。
这时,exit_flag都需要使用volatile来修饰。不然对于线程1的代码,如果编译器发现在线程1的代码中没有任何地方修改exit_flag,有可能会将exit_flag放入寄存器缓存。这样,在每次的条件检查的时候,都是从寄存器中读取,而非exit_flag对应的内存。这样将导致每次读取的值都为0,从而导致thread1无法退出。而使用volatile修饰exit_flag,会避免编译器做这种优化,强制每次读取都是从内存中读取,这样就可以保证了在exit_flag置为1时,thread1可以读取到最新值而退出。
那么现在有这样的一个问题。当有一个全局变量volatile int counter = 0作为一个计数器,而且同时有两个线程会修改这个计数器,这时这个counter是否需要锁的保护呢?
比如下面的代码:
- static volatile int counter = 0;
- void add_counter(void)
- {
- ++counter;
- }
在多线程的环境下,是否安全呢?counter的递增是否为我们所期待的结果呢?请大家先想一分钟!
为了得出答案,让我们看汇编代码:
- add_counter:
- pushl %ebp
- movl %esp, %ebp
- movl counter, %eax
- addl $1, %eax
- movl %eax, counter
- popl %ebp
- ret
看红色的这三行。怎么回事?为什么还是先把counter存入了寄存器eax,然后对eax进行操作,再将eax存入counter。这样的话,++counter就绝不会是原子性操作!必须由锁保护!
下面看看为什么汇编是这样的行为。究竟volatile提供了什么样的服务?!
首先修改一下之前的C代码:
- static int counter = 0;
- void add_counter(void)
- {
- for (; counter != 0x10; ++counter) {
- ++counter;
- }
- }
然后看其汇编代码,记得要是使用-O优化啊。
[fgao@fgao-vm-fc13 test]$ gcc -S -O test.c
- add_counter:
- pushl %ebp
- movl %esp, %ebp
- movl counter, %eax
- cmpl $16, %eax
- je .L4
- .L5:
- addl $2, %eax
- cmpl $16, %eax
- jne .L5
- movl %eax, counter
- .L4:
- popl %ebp
- ret
从上面的汇编代码中,可以很清楚的看出:这时将counter的值存入eax,然后每次给eax加2,然后使用eax与16比较,来完成C代码中的for循环的工作。
下面给counter加上volatile修饰符
- static volatile int counter = 0;
- void add_counter(void)
- {
- for (; counter != 0x10; ++counter) {
- ++counter;
- }
- }
依然打开编译器的优化开关
[fgao@fgao-vm-fc13 test]$ gcc -S -O test.c
- add_counter:
- pushl %ebp
- movl %esp, %ebp
- movl counter, %eax
- cmpl $16, %eax
- je .L4
- .L5:
- movl counter, %eax
- addl $1, %eax
- movl %eax, counter
- movl counter, %eax
- addl $1, %eax
- movl %eax, counter
- movl counter, %eax
- cmpl $16, %eax
- jne .L5
- .L4:
- popl %ebp
- ret
L5为对应C代码中的for循环的汇编实现。通过对比前一个汇编,我们很容易就发现了区别。虽然在执行counter递增的时候,两者都使用了寄存器eax。但是在每次访问counter的时候,前者会直接使用寄存器eax,而后者(使用volatile的时候)会重新从counter中读取最新值至eax,然后再使用eax。
OK。现在volatile的情况已经明了。volatile只提供了保证访问该变量时,每次都是从内存中读取最新值,并不会使用寄存器缓存该值——每次都会从内存中读取。而对该变量的修改,volatile并不提供原子性的保证。那么编译器究竟是直接修改内存的值,还是使用寄存器修改都符合volatile的定义。所以,一句话,volatile并不提供原子性的保证。