先抛出结论:c 语言 volatile 关键字的作用在于提示编译器,这个变量值可能被其他修改,在取值时要从内存读取。
后续会解释,从内存中读取是什么意思。
构造这样一种情况,在多线程环境下,有一个全局变量 i 初始值为 0,线程 A 轮寻 i 的值,当 i 的值为 1 时退出;线程 B 在某一时刻改变 i 的值,然后退出。我们期望无论 A B 线程执行顺序如何,线程 A B 都可以正确的退出。
测试代码
1 #include<stdlib.h>
1 #include<pthread.h>
2
3 int i=0;
4
5 void* threadA() {
6 for(;!i;) {}
7 }
8
9 void* threadB() {
10 i=1;
11 }
12
13
14 int main() {
15 pthread_t a;
16 pthread_t b;
17
18 pthread_create(&a, NULL, threadA, NULL);
19 pthread_create(&b, NULL, threadB, NULL);
20
21
22 void* ret;
23 pthread_join(a, &ret);
24 pthread_join(b, &ret);
25 }
如果将这段代码编译运行,程序不会退出。
merore@merore-pc:/wk/wk/codebook/volatile/c$ ./no-volatile.out
加上 volatile
1 #include<stdlib.h>
1 #include<pthread.h>
2
3 volatile int i=0;
4
5 void* threadA() {
6 for(;!i;) {}
7 }
8
9 void* threadB() {
10 i=1;
11 }
12
13
14 int main() {
15 pthread_t a;
16 pthread_t b;
17
18 pthread_create(&a, NULL, threadA, NULL);
19 pthread_create(&b, NULL, threadB, NULL);
20
21
22 void* ret;
23 pthread_join(a, &ret);
24 pthread_join(b, &ret);
25 }
程序正常退出
merore@merore-pc:/wk/wk/codebook/volatile/c$ ./volatile.out
merore@merore-pc:/wk/wk/codebook/volatile/c$
那么 volatile 究竟起一个什么效果呢?将这两段代码分别编译成汇编文件
merore@merore-pc:/wk/wk/codebook/volatile/c$ make
gcc -Os -S -o no-volatile.s no-volatile.c
gcc -Os -S -o volatile.s volatile.c
带 volatile
8: 线程 A 加载变量 i 的地址到 r13 寄存器
10: 读取 r13 的值所指向的内存,即从内存中读取 i 的值
11: 如果值为 0 ,跳到 .L2,然后再次读取循环,否则继续执行 12 行,退出线程,这里 jr $r1 相当于 c 语言 return。
可以看到,这个程序逻辑和我们期望的是一致的,一旦 i 的值发生改变,总可以读到 i 的新值然后退出线程。
5 threadA:
6 .LFB6 = .
7 .cfi_startproc
8 la.local $r13,i
9 .L2:
10 ldptr.w $r12,$r13,0
11 beqz $r12,.L2
12 jr $r1
13 .cfi_endproc
14 .LFE6:
15 .size threadA, .-threadA
16 .align 2
17 .globl threadB
18 .type threadB, @function
不带 volatile
8: 加载变量 i 的地址到 r12
9: 从内存读取变量 i 的值到 r12
10: 如果 r12 不等于 0,则跳到 .L5 即退出线程,否则,继续走到 .L4。一旦程序走到 12 行,将陷入循环。不会再次读取值做比较。
可以看到,在不带 volatile 的情况下,程序行为和我们预期的不一致,程序无法正常退出。
5 threadA:
6 .LFB6 = .
7 .cfi_startproc
8 la.local $r12,i
9 ldptr.w $r12,$r12,0
10 bnez $r12,.L5
11 .L4:
12 b .L4
13 .L5:
14 jr $r1
15 .cfi_endproc
16 .LFE6:
17 .size threadA, .-threadA
18 .align 2
19 .globl threadB
20 .type threadB, @function
总结
这里就明白了,要从内存中读取被 volatile 修饰的变量是什么意思
。总之,在并发环境下,共享变量都应该加上 volatile
修饰。否则会因为 gcc 的一些编译优化行为造成语义不一致。
网友评论