目录
前言
最近在工作中遇到了一个问题,定位后发现是由于编译器优化了一个全局变量,导致依赖于该全局变量的判断逻辑未发生。下来我们上代码一起来看一下。
提示:以下内容基于C语言讲解。
一、下面代码逻辑上是否有问题?
#include <stdio.h>
#include <pthread.h>
...
bool runFlag = TRUE;
bool exitFlag = FALSE;
void threadFunc(void *arg)
{
while(runFlag)
{
...
}
exitFlag = TRUE;
}
void exitThread(void)
{
runFlag = FALSE;
for(; ;)
{
if (TRUE == exitFlag)
{
...
}
}
}
void main(void)
{
...
pthread_create(&tid, &attr, threadFunc, NULL);
sleep(100);
exitThread();
}
二、认识volatile
volatile是一个特征修饰符。它的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,并且要求每次直接读值。
什么叫直接读值呢?我们可以通过反汇编来看看,上述代码被编译器编译之后是什么样子的。
*****************************************************************************/
void exitThread(void)
{
0: e92d47f0 push {r4, r5, r6, r7, r8, r9, sl, lr}
4: e1a04000 mov r4, r0
runFlag = FALSE;
8: e3a00000 mov r0, #0
c: e1a05104 lsl r5, r4, #2
10: e59f3120 ldr r3, [pc, #288] ;
14: e0852004 add r2, r5, r4
18: e08f3003 add r3, pc, r3
1c: e0822282 add r2, r2, r2, lsl #5
20: e59f1114 ldr r1, [pc, #276] ;
24: e0833102 add r3, r3, r2, lsl #2
28: e08f1001 add r1, pc, r1
2c: e5933014 ldr r3, [r3, #20]
30: e7810104 str r0, [r1, r4, lsl #2]
for (; ;)
{
if (TRUE == exitFlag)
34: e3530001 cmp r3, #1
38: 1afffffd bne 34
{
......
通过arm-linux-objdump -D -S *.o命令(目标文件为上述代码编译产生的目标文件,objdump需要使用目标机器工具链)来反汇编,同时对照C语言,我们可以发现,编译之后的代码,在函数进来之后,将exitFlag的值取出来放在了r3寄存器中,然后进入for循环后一直使用的是寄存器r3中的值,这就导致,当内存中exitFlag的值被更改后不会同步到exitThread函数中,从而产生与预期不一样的结果。
所以,上面说的每次读值的意思是:告诉编译器这个值可能随时会被改变,在用到这个变量时必须每次都从内存中重新读取这个变量的值,而不是使用保存在寄存器中的备份。
那么编译器为什么会做这个优化呢?原来,编译器在编译的时候,会根据优化级别来优化我们的代码,为了提高访问速度,编译器会有选择的将访问变量的值先读取到寄存器中,待执行完一定的步骤后再写回内存或重新从内存中读取到寄存器。那么在这个过程中,可能就会带来灾难性的后果。
总结
1、遇到多线程共享的变量时,可以用volatile关键字告诉编译器不要进行优化。
2、映射在存储器中的硬件寄存器,如状态寄存器需用volatile来修饰,因其每次都有不同的含义。
3、在中断服务程序中修改的供其它程序检测的变量需要加volatile;