实例观察 c 语言中 volatile 的作用

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 
#include 
#include 
#include 

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 的值。

实例观察 c 语言中 volatile 的作用_第1张图片

无 volatile 修饰:

从汇编指令可以看出,while() 循环中,编译器只在 1234, 123b 两个指令中从内存中读取数据,然后进行比较,后边直接在 123d 这一条指令进行循环,并不是每次循环都从内存中读取数据。

实例观察 c 语言中 volatile 的作用_第2张图片

你可能感兴趣的:(c语言)