1 实例代码分析

在讲解volatile关键字的作用之前,我们先来看一个代码的例子,代码如下:

  • main.c
#include <stdio.h>
#include <pthread.h>

extern const int g_ready;

int main()
{
    launch_device();


    while( g_ready == 0 )
    {
        sleep(1);
        
        printf("main() : g_ready = %d\n", g_ready);
    }
    
    printf("main() : g_ready = %d\n", g_ready);

    return 0;
}
  • device.c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int g_ready = 0;

void* th_fn(void* args)
{
    sleep(5);

    g_ready = 1;

    printf("th_fn() : g_ready = %d\n", g_ready);
}

void launch_device()
{
    pthread_t tid = 0;

    pthread_create(&tid, NULL, th_fn, NULL);
}

上面的代码,是非常容易懂的,这里就不再多说。我们直接编译运行看看:

  • gcc -pthread main.c device.c -o test.out
  • ./test.out

运行结果如下:

这个结果对于我们来说其实挺容易懂的。就是main中的while循环先每隔一秒打印执行一次,然后5秒后th_fn函数开始执行,th_fn函数将g_ready变量变为1,然后打印一个语句。最后回到main中的while循环,由于while中上次最后一秒现在才执行完,打印一句,然后由于此时g_ready为1,所以退出循环,然后打印循环外的一个语句,然后结束程序的运行。这种结果,其实是比较容易理解的。也是比较普遍的结果。

但是如果我们再编译上述代码的时候,加上了-O3选项,该选项是优化选项,且级别比较高。编译运行结果如下:

  • gcc -O3 -pthread main.c device.c -o test.out
  • ./test.out

运行结果如下动态图:

由以上结果我们可以看到,程序陷入了死循环,g_ready没有被改变一直都是0,所以main中的while循环一直在执行。对于这个结果,可能并不是所有人都知道为什么。下面,我们就要来讲解为什么会产生上面的运行结果。

2 问题分析

-O3选项是让编译器对代码进行优化。

  • 编译优化时,编译器根据当前文件进行优化
  • 为了效率上的提高,优化时编译器将变量值从内存中读取进入寄存器
  • 每次要访问变量时直接从寄存器读取。毕竟寄存器的存取比内存的存取快很多。

那么,我们明白了优化对变量的影响。对于上述的代码,如果在编译时加了-O3选项。编译优化时,编译器会将变量g_ready的值放到寄存器中,以后每次使用该变量就从寄存器中取出g_ready的值使用即可。这样虽然是速度快了,但是当在th_fn函数中改变了g_ready的值后,在内存中确实g_ready的值已经变为1了,但是在刚刚那个寄存器中,最开始将g_ready的值也就是0存进去的,一直都没有改变过,但是呢,由于编译器的优化,每次在使用g_ready的变量的时候,都是从寄存器中直接取出值来使用…

说到这里,应该都能够明白了,此时从寄存器中取出的值就一直都是0.然后while一直循环。

3 解决方案

编译的时候使用-O3选项是很常用的。那么如何才能既使用这个-O3选项,又使得上述程序按照我们的意愿来执行呢?volatile就此出场。

使用volatile关键字修饰可能被意外修改的变量(内存),从而禁止编译器对该变量进行优化。

  • volatile一般修饰的是一种易变的变量
  • volatile可理解为一种编译器警告指示字,它告诉编译器必须每次都直接去内存中取变量值。

那么在上述的代码中,我们将main.c中的extern const int g_ready; 改为:extern volatile const int g_ready; 再重新进行优化的编译,然后运行,结果就是正确的运行。可以自己尝试做实验!

4 拓展: const和volatile

细心的朋友会发现,在main.c程序中,我们改完后,成这样了:extern volatile const int g_ready; 又是const又是volatile的。我们上面说过,volatile修饰的是意变的变量,而const修饰的变量是不能被修改的,有的人叫它常量,不可变的。而实际上在编程语言中的解释是这样的:const修饰的变量在当前文件中不能够出现在赋值符号的左边。 所以我们可以看到,在main.c这个文件中g_ready这个变量,并没有出现在赋值符号的左边,所以是没有问题。但是在device.c文件中g_ready是没有被const修饰的,它就可以出现在赋值符号的左边,可以被改变。这样一来,在device.c中修改g_ready这个变量,在main.c中的g_ready也间接被改变了。所以,我觉得说const修饰的是常量这个说法不够准确,说它是变量但是不能够出现在赋值符号的左边更加准确。

下面就总结一下const和volatile关键字:

  • const表示修饰的变量在当前文件中不能出现在赋值符号的左边,不能直接被改变,但是可以间接被改变
  • volatile表示修饰的变量每次使用的时候直接从内存中读取
  • const和volatile同时修饰变量时互不影响。

4 总结

  • 编译优化时,编译器仅根据当前文件进行优化
  • 编译器的优化策略可能造成一些意外
  • volatile强制编译器必须从内存中取变量值
  • const和volatile同时修改变量时,互相不影响彼此的含义。