volatile在C语言中的基本使用方法

简介: 大家好,我是ST。学C语言时,有一个奇怪的关键字volatile,这到底有什么用呢?

第一:volatile与编译器

首先,我们来看这样一段代码:

int busy = 1;
void wait() {
while(busy) {
   ;
 }
}

编译一下,注意,这里使用O2优化

c8c6bba6cfe44097a1f1aea1e0049599.png

下面,让我们仔细看看生成的这段汇编:

wait:
        mov     eax, DWORD PTR busy[rip]
.L2:
        test    eax, eax
        jne     .L2
        ret
busy:
        .long   1

其中L2这一段即为while循环,这段指令是经过编译器优化的,可以看到,决定能否跳出循环是通过检查寄存器eax来完成的,而没有检查变量busy所在内存的真实内容。


注意,对于这段代码来说这里的优化是正确的,但问题是如果还有其它代码修改了变量busy,那么这里的优化会导致其它代码对变量busy的修改根本就不能生效,就像这样:

int busy = 1;
// 该函数在A线程中执行
void wait() {
    while(busy) {
        ;
    }
}
// 该函数在B线程中执行
void signal() {
    busy = 0;
}

如果wait函数中while循环对应的机器指令仅仅从寄存器中读取数据那么即使B线程的signal函数修改了busy变量也不能让wait函数从循环中跳出来。


如果你对busy变量使用volatile修饰,生成的指令就变成这样了:

5245d5943f646d99edc89331a8eb7a8f.png

wait:
.L2:
        mov     eax, DWORD PTR busy[rip]
        test    eax, eax
        jne     .L2
        ret
busy:
        .long   1

注意看此时L2这一段,每次都从busy变量所在的内存中读取数据并存放在eax,然后再去判断,这样就能确保每次都能读取到busy变量的最新值。


实际上,你可以把寄存器eax当做busy所在内存的cache,当cache(寄存器)和内存中的数据一致时不会有任何问题,但当cache与内存中的数据不一致时(也就是内存已被更新但cache保存的还是旧数据),程序的运行往往出乎预料。


除了多线程的例子,还有一类就是signal handler以及硬件修改该变量(用C语言与硬件交互式时经常遇到),如果编译器生成文章开头那样的指令那么等待线程将检测不到signal handler或者硬件对变量的修改。


f6acee38d97b3513dfa4d0f8ad191e79.png因此,在这里我们需要告诉编译器:“不要耍小聪明,不要只从寄存器中读数据,这个变量可能在其它地方已经被修改了,使用时从内存中获取最新数据”。


现在是时候简单总结一下了,volatile仅仅阻止编译器试图去优化对变量的读取操作

第二:volatile与多线程

一定要注意volatile仅仅确保变量的可见性,但和变量的原子访问没有半毛钱关系,这是两个完全不同的任务

假设有一个非常复杂的结构体struct foo:

struct data {
  int a;
  int b;
  int c;
  ...
};
volatile struct data foo; 
void thread1() {
    foo.a = 1;
    foo.b = 2;
    foo.c = 3;
    ...
}
void thread2() {
    int a = foo.a;
    int b = foo.b;
    int c = foo.c;
    ...
}

你仅仅用volatile去修饰变量foo只是确保了当该变量被thread1修改后我们能在thread2中读取到最新值,但这解决不了多线程并发读写需要原子访问foo的问题。


确保变量原子性访问一般都采用锁,当使用锁时,锁本身就包含了volatile提供能力,即,确保变量的可见性,因此当使用锁时没有必要使用volatile。

第三:volatile与memory order

有的同学可能会想,如果我想用volatile修饰的变量没有那么复杂,仅仅是一个int,就像这样:

volatile int busy = 0;

A线程读取busy变量,B线程更新busy变量,当A检测到busy变化后执行特定操作,这样可行吗?既然通过volatile修饰后可以确保每次都从内存中读取busy,那么应该可以这样使用吧。


然而,计算机在概念上可能相对简单些,但在工程实践中是复杂的。


我们知道,由于CPU与内存之间的速度差异非常大,CPU与内存之间有一层cache,CPU其实并没有直接读取内存,cache的存在会让问题复杂起来,限于篇幅与本文主题这里不再展开。


为优化内存读写,CPU可能会对内存读写操作进行指令重排,reordering,带来的后果就是:假设在线程1中先后执行第N行代码与第N+1行代码,但在线程2看来却是第N+1行代码先生效,假设X的初始值为0,Y的初始值为1:

线程1           线程2
X = 10         if (!busy)
busy = 0;         Y = X;

当线程2检测到busy为0后读取X的值,此时读取到的X值可能为0。

为解决这一问题,我们需要的不是volatile,volatile解决不了reordering问题,我们需要的是内存屏障,memory barrier。

内存屏障是一类机器指令,该指令对处理器在该屏障指令之前与之后的内存操作进行了限制,确保不会出现重排问题。

而内存屏障带来的效果依然能够涵盖volatile提供的功能,因此也不需要volatile。

可以看到,在多线程环境下我们几乎总是不会使用volatile关键字。


目录
相关文章
|
存储 C语言
C语言之数组的定义及其使用方法
C语言之数组的定义及其使用方法
151 0
|
5月前
|
C语言
【C语言】:const的使用方法
【C语言】:const的使用方法
25 0
|
5月前
|
存储 C语言
C语言数组指针和指针数组的区别及使用方法
C语言数组指针和指针数组的区别及使用方法
87 0
|
存储 C语言
【C语言】memcpy , memset等内存操作函数使用方法与注意事项
【C语言】memcpy , memset等内存操作函数使用方法与注意事项
185 0
|
6月前
|
编译器 C语言 Python
详解C语言指针的使用方法(下)
详解C语言指针的使用方法(下)
74 0
|
6月前
|
存储 安全 编译器
详解C语言指针的使用方法(上)
详解C语言指针的使用方法(上)
68 0
|
编译器 C语言
C语言之回调函数,qsort函数的定义及使用方法
C语言之回调函数,qsort函数的定义及使用方法
|
存储 C语言
C语言之指针的含义,指针类型的定义及使用方法,野指针的定义,以及原因,如何避免野指针
C语言之指针的含义,指针类型的定义及使用方法,野指针的定义,以及原因,如何避免野指针
|
缓存 Unix Linux
【C语言】详解getchar和putchar的使用方法
我们知道scanf函数可以从键盘输入信息,而printf则可以输出信息,同样地,getchar和putchar也有同样的功能。下面我来给大家介绍putchar和getchar的使用方法。
328 0
|
C语言
【C语言】深度讲解 atoi函数 使用方法与模拟实现
【C语言】深度讲解 atoi函数 使用方法与模拟实现
207 0