volatile关键字

特性

volatile关键字一共三个特性

  1. 易变性。所谓的易变性,在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取。
  2. “不可优化”特性。volatile告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。
  3. ”顺序性”,能够保证Volatile变量间的顺序性,编译器不会进行乱序优化。 C/C++ Volatile变量,与非Volatile变量之间的操作,是可能被编译器交换顺序的。C/C++ Volatile变量间的操作,是不会被编译器交换顺序的。哪怕将所有的变量全部都声明为volatile,哪怕杜绝了编译器的乱序优化,但是针对生成的汇编代码,CPU有可能仍旧会乱序执行指令,导致程序依赖的逻辑出错,volatile对此无能为力 针对这个多线程的应用,真正正确的做法,是构建一个happens-before语义。

易变性

volatile的易变性是为了禁用编译器的缓存优化。强制程序每次都从内存里读取变量。而不是从可能已经过时的寄存器副本中读取(我的理解是为了加速读取编译器会从cpu缓存进行变量读取。如果高并发的情况可能会出现问题)。 如何理解易变性。可以举一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
int flag = 0; // 没有 volatile 修饰
// 线程 1
void waitForFlag() {
    while (flag == 0) {
        // 空循环,等待flag变为1
    }
    printf("Flag is set!\n");
}

// 线程 2
void setFlag() {
    flag = 1; // 另一个线程修改flag
}

没有设置volatile关键字。编译器优化flag后

  1. flag没有设置volatile关键字
  2. 线程1执行waitForFlag它会从内存读flag值为0
  3. 于是,编译器“聪明地”决定,与其在每次循环时都执行一次昂贵的内存访问操作,不如把 flag 的初始值(0)加载到一个寄存器中,然后循环就变成了反复检查这个寄存器。
  4. 此时,即使线程 2 在另一个 CPU 核心上执行了 setFlag(),将内存中的 flag 真实值改为了 1,线程 1 也浑然不知,因为它还在检查那个已经被“缓存”在寄存器里的旧值 0。这就导致了一个无限循环。

设置volatile关键字后行为

  1. 编译器被告知 flag 是易变的。
  2. 因此,它不敢再把 flag 的值缓存到寄存器里。
  3. 每次在代码中需要 flag 的值时(比如 while (flag == 0)),编译器都必须生成一条从内存中重新读取 flag 的指令。

不可优化性

不可优化性比较好理解。对于release的程序,编译器会对代码变量进行优化包括不限于变量直接消除,优化掉多余debug信息等。有时候gdb debug的根本看不到变量具体的值。

顺序性

举例子

Happens-Before就是保证你能看到的规则。 举简单例子: 想象你在看朋友发朋友圈 场景1:没有Happens-Before(出问题) 你的朋友:先做饭,然后拍照,最后发朋友圈
你看到的是:照片先出来,但是照片里的饭还没做出来
这就是重排序问题:操作顺序乱了

场景2: 有Happens-Before(正常) 你朋友:先做饭(A),然后拍照(B),最后发朋友圈(C)
规则:发朋友圈 happens-before 你看到朋友圈
你看到的是:先看到朋友圈,里面的照片显示做好的饭
一切都按正确顺序出现!

代码例子

1
2
3
4
5
6
7
8
9
// 线程1
x = 1;          // 操作A
flag = true;    // 操作B (volatile)

// 线程2
if (flag) {     // 操作C (看到flag为true)
    print(x);   // 操作D
}

Happens-Before 链:A->B->C->D

线程1 程序按照顺序执行从A->B 线程2

  1. 普通情况
    因为编译器是乱序的因此可能出现代码是乱序的情况。先操作B再操作A。
1
2
3
flag = true;    // 操作B
x = 1;          // 操作A

当然对于编译器是对单线程负责的,因为不知道还有线程2。对线程1如此优化完全没问题的。 因此结果是操作D没有看到x=1

  1. volatile情况
    因为设置了volatile。编译器不会进行激进的优化,因此线程1会按照顺序A->B 线程2会按照C->D顺序。 因此我们建立了完整的因果关系从A->D的顺序