C语言volatile关键字,

C语言volatie关键字

1. 源起

编译器把C源文件编译成目标文件的时候,可能会启用优化。

例如下面代码:

int i = 10;

int main() {
	int j = i;
	...
	int k = i;
}

由于编译器发现两次从i读取数据的代码之间的代码没有对i进行过操作,它会自动把上次读取的数据(一般在寄存器中)放入k中,而不是重新从i里面读取。

然而,编译器对代码的优化并不具备真正的智能,有些情况下变量i的值在int j = i;int k = i;之间改变了但编译器却不能感知到。这样的情况很多,譬如:

  • 多线程程序中,另一个线程改变了全局变量i的值
  • 嵌入式开发时,在一个中断函数中改变了全局变量i的值
  • 嵌入式开发时,i表示一个硬件寄存器,CPU可能通过硬件直接改变i的值
  • 。。。

在这些情况下,编译器优化后的目标代码就是不正确的。这个时候就需要volaitle关键字出场了。


2. volatile关键字

功能:用于修饰变量,它告诉编译器变量的值随时可能发生变化的(即:易变的),每次使用变量的时候必须从变量的地址中重新读取,从而避免了编译器对读取变量的代码执行之前所说的优化。

使用示例:

volatile int i = 10;

int main() {
	int j = i;
	...
	int k = i;
}

一个未使用volatile导致错误的例子

程序源文件如下:

// test.c
// main函数中先后两次读取i的值分别放入a、b,最后比较a和b的值并打印。在两次读取i值之间,通过另一个线程改变i的值。

#include <stdio.h>
#include <pthread.h>

double x = 3;
void wait_long_time() {
    for (int64_t n = 0; n < 9999999999; ++n) {
        x += 123;
    }
}

int i = 10;

void *thread_entry(void *param) {
    sleep(3);
    printf("thread modify i\n");
    i = 6;
    return NULL;
}

int main(void){
    int a, b;
    pthread_t t;
	
	// 创建一个线程t,该线程内延迟3秒后修改i的值
    pthread_create(&t, NULL, thread_entry, NULL); 

    a = i;
    wait_long_time(); // 等待一段时间,在该时间内线程t修改了i的值
    b = i;
 
    if(a == b){
        printf("a = b\n");
    }
    else {
        printf("a != b\n");
    }

    pthread_join(t, NULL);
    
    return 0;
}

首先使用默认优化级别编译运行:

$ gcc test.c -o test
$ ./test 
thread modify i
a != b

程序输出a!=b,正确。

然后使用-O2优化级别编译运行:

$ gcc -O2 test.c -o test
$ ./test 
thread modify i
a = b

程序输出a=b,错误。原因自然已经明了。


解决这个问题的方案就是:把int i = 10;改为volatile int i = 10;

修改后再次使用-O2优化级别编译运行:

$ gcc -O2 test.c -o test
$ ./test 
thread modify i
a != b

结果正确,说明volatile使得编译器对读取变量i的代码没有进行优化。


3. volatile关键字什么情况下要用

摘自:https://blog.csdn.net/weixin_38815998/article/details/102840096

3.1 自定义延时函数

#include <stdio.h>
 
void delay(long val);
int main(){
					
	delay(1000000);
 
	return 0;
}
 
void delay(long val){
 
	while(val--);
}

相信大佬们对如上程序都挺熟悉的,特别是玩过单片机的同学,主要是通过CPU不断进行无意义的操作达到延时的效果,这种操作如果不启用编译器优化是可以达到预期效果的,但是启用编译器优化就会被优化成如下效果(当然不是在C语言层面上优化,而是在汇编过程优化,只是使用C程序举例):

#include <stdio.h>
 
void delay(long val);
int main(){
					
	delay(1000000);
 
	return 0;
}
 
void delay(long val){
 
	;
}

这个时候,delay函数就起不了效果了,需要使用 volatile 修饰 val ;具体可见:
编译器优化对自定义延时程序的影响(volatile详解实验一)

3.2 多线程共享的全局变量

编译器优化对多线程数据同步的影响(volatile详解实验二)

3.3 中断函数与主函数共享的全局变量

中断函数和主函数共享的全局变量需要使用 volatile 修饰的情况是相似的。大家可以感受实验二,去做一个中断的实验。

3.4 硬件寄存器

什么叫硬件寄存器,学过硬件的同学应该不陌生,我们在做按键检测的时候是不是下面这种流程:

1.设置GPIO对应的寄存器配置成输入模式
2.不断地去访问GPIO电平标志寄存器(或者是一个寄存器的标志位)
3.根据寄存器值的某个二进制位确定当前引脚电平。
.
那么有没有想过一个问题,是什么去改变硬件寄存器的值?其实,硬件寄存器上的值的是和底层电路相关的,硬件寄存器的值会影响电路,电路也会反过来影响硬件寄存器的值。
所以在这种情况下,编译器更不应该拷贝副本,而应该每次读写都从内存中读写,保证数据正确,声明成volatile可以防止出现数据出错问题。


附:关于编译器的优化级别

摘自:https://blog.csdn.net/guanlizhongxintishi/article/details/124644480

  1. gcc中指定优化级别的参数有:-O0、-O1、-O2、-O3、-Og、-Os、-Ofast。
  2. 在编译时,如果没有指定上面的任何优化参数,则默认为 -O0,即没有优化。
  3. 参数 -O1、-O2、-O3 中,随着数字变大,代码的优化程度也越高,不过这在某种意义上来说,也是以牺牲程序的可调试性为代价的。
  4. 参数 -Og 是在 -O1 的基础上,去掉了那些影响调试的优化,所以如果最终是为了调试程序,可以使用这个参数。不过光有这个参数也是不行的,这个参数只是告诉编译器,编译后的代码不要影响调试,但调试信息的生成还是靠 -g 参数的。
  5. 参数 -Os 是在 -O2 的基础上,去掉了那些会导致最终可执行程序增大的优化,如果想要更小的可执行程序,可选择这个参数。
  6. 参数 -Ofast 是在 -O3 的基础上,添加了一些非常规优化,这些优化是通过打破一些国际标准(比如一些数学函数的实现标准)来实现的,所以一般不推荐使用该参数。
  7. 如果想知道上面的优化参数具体做了哪些优化,可以使用 gcc -Q --help=optimizers 命令来查询,比如下面是查询 -O3 参数开启了哪些优化:
    $ gcc -Q --help=optimizers -O3
      ...
      -fassociative-math              [disabled]
      -fassume-phsa                   [enabled]
      ...
    

有关gcc优化的更多详细信息,请参考gcc的官方文档:
https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值