volatile 意思是易变的。
在 c 语言中,如果变量被 volatile 修饰,就是告诉编译器这个变量随时都可能发生变化,那么每次读取变量的时候都会到内存中读取。
如果变量没有被 volatile 修饰,并且编译器发现在多次读取变量之间,变量没有被修改,那么编译器可能将变量的值保存到寄存器中,这样在后边访问变量的时候性能会得到提升。但是如果变量以编译器无法识别的方式被修改,那么这个时候将变量的值保存在寄存器中就可能引入问题。
1 volatile 使用场景
(1)硬件寄存器映射的内存地址
在嵌入是开发中,cpld 或者 fpga 中的寄存器可以映射到内存地址,直接通过内存地址来访问这些寄存器。这些寄存器的值可能会因硬件原因发生改变,这种情况是编译器无法识别的,所以需要 volatile 进行修饰。
(2)中断服务程序中会修改的,并且其它线程会访问的变量,需要使用 volatile 进行修饰。
(3)多线程应用中,多个线程共享的变量,需要使用 volatile 进行修饰。
volatile 是对编译器优化的补充。
在接触 volatile 之前,总会想当然的认为读取一个变量的时候就会去内存地址中读取。其实不不然,编译器会进行优化,如果在多次读取变量之间没有发现变量被修改,编译器可能将变量的值保存到寄存器中,这样后边的读取直接从寄存器中读取。从寄存器中读取相对于从内存中读取,性能大大提升。
但是编译器优化也不是万能的,当改变变量的方式,编译器无法识别的时候,那么编译器优化会导致问题。比如上边这 3 中场景,在这些场景下使用的变量,就需要使用 volatile 进行修饰。
2 实例观察有无 volatile 的区别
如下代码中,有一个全局变量 int flag,初始值是 1。
创建了两个线程 t1 和 t2,在 t1 中有一个 while() 循环,当 flag 的值为 1 的时候,循环继续;否则循环退出,并打印 "flag is not 1"。在 t2 中,将 flag 修改成了 0。
(1)int flag 被 volatile 修饰
编译命令:
gcc -O3 volatile.c -g
编译之后运行程序,会看到当 flag 被设置为 0 之后,t1 线程中的循环退出,并且打印了 "flag is not 1"。
(2)int flag 没有被 volatile 修饰
还是使用相同的命令进行编译。
编译之后运行程序,当 flag 被设置为 0 之后,看不到 t1 线程中的循环退出。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>volatile int flag = 1;void *thread_entry(void *data) {while (flag == 1) {int tmp = flag;}printf("flag is not 1\n");
}void *thread_entry1(void *data) {int *p = (int *)data;*p = 0;printf("after set flag to 0\n");
}int main() {pthread_t t1;pthread_create(&t1, NULL, thread_entry, NULL);sleep(10);pthread_t t2;pthread_create(&t2, NULL, thread_entry1, &flag);sleep(100);return 0;
}
通过上边两个实验,我们可以看出来加 volatile 和不加 volatile 的区别,与前边分析的一致。
我们通过将可执行文件进行反汇编,通过汇编指令来分析加 volatile 和不加 volatile 之间的区别。
有 volatile 修饰:
从汇编指令中可以看出来,while() 循环,在 1248 到 1257 指令之间循环执行,每次执行的时候,都会到内存中读取 flag 的值。
无 volatile 修饰:
从汇编指令可以看出,while() 循环中,编译器只在 1234, 123b 两个指令中从内存中读取数据,然后进行比较,后边直接在 123d 这一条指令进行循环,并不是每次循环都从内存中读取数据。