从汇编角度理解 volatile 关键字
1. 什么是 volatile 关键字
volatile 是 C/C++ 中的一个关键字,用于告诉编译器:
- 该变量的值可能会被程序之外的因素修改(如硬件、其他线程等)
- 编译器不应对该变量进行某些优化(如缓存到寄存器)
- 每次使用该变量时都应该从内存中重新读取
2. 问题现象
假设有以下场景:
- 全局变量 i 初始值为 0
- 线程 A 轮询 i 的值,当 i 为 1 时退出
- 线程 B 在某一时刻将 i 的值改为 1,然后退出
- 主线程等待 A 和 B 都退出后再退出
2.1 不带 volatile 的代码
#include<stdlib.h>
#include<pthread.h>
int i = 0;
void* threadA() {
for(; !i;) {}
}
void* threadB() {
i = 1;
}
int main() {
pthread_t a;
pthread_t b;
pthread_create(&a, NULL, threadA, NULL);
pthread_create(&b, NULL, threadB, NULL);
void* ret;
pthread_join(a, &ret);
pthread_join(b, &ret);
}
编译并运行,程序可能会卡住无法正常结束。
2.2 带 volatile 的代码
#include<stdlib.h>
#include<pthread.h>
volatile int i = 0;
void* threadA() {
for(; !i;) {}
}
void* threadB() {
i = 1;
}
int main() {
pthread_t a;
pthread_t b;
pthread_create(&a, NULL, threadA, NULL);
pthread_create(&b, NULL, threadB, NULL);
void* ret;
pthread_join(a, &ret);
pthread_join(b, &ret);
}
编译并运行,程序总能正常结束。
3. 从汇编角度分析
3.1 生成汇编代码
gcc -Os -S -o no-volatile.s no-volatile.c
gcc -Os -S -o volatile.s volatile.c
3.2 分析不带 volatile 的汇编
threadA:
.LFB6 = .
.cfi_startproc
la.local $r13,i
.L2:
ldptr.w $r12,$r13,0
beqz $r12,.L2
jr $r1
.cfi_endproc
分析:
1. 线程 A 加载变量 i 的地址到 r13 寄存器
2. 进入循环,从内存中读取 i 的值到 r12 寄存器
3. 如果值为 0,跳回 .L2 继续循环
4. 否则退出线程
3.3 分析带 volatile 的汇编
threadA:
.LFB6 = .
.cfi_startproc
la.local $r12,i
ldptr.w $r12,$r12,0
bnez $r12,.L5
.L4:
b .L4
.L5:
jr $r1
.cfi_endproc
分析:
1. 线程 A 加载变量 i 的地址到 r12 寄存器
2. 从内存中读取 i 的值到 r12 寄存器
3. 如果值不为 0,跳转到 .L5 退出线程
4. 否则进入 .L4 死循环,不再重新读取内存中的值
4. 为什么会出现这种情况?
编译器在优化时,会假设变量的值只会在当前线程内被修改。对于不带 volatile 的变量,编译器可能会:
- 寄存器缓存:将变量的值缓存到寄存器中,避免每次都从内存读取
- 循环优化:如果编译器认为循环条件不会改变,可能会将循环优化为死循环
- 代码重排序:调整代码执行顺序以提高性能
在多线程环境中,这些优化可能导致线程无法看到其他线程对变量的修改,从而出现意想不到的行为。
5. volatile 的作用
volatile 关键字告诉编译器:
- 禁止寄存器缓存:每次使用变量时都必须从内存中读取
- 禁止循环优化:确保循环条件会被重新评估
- 禁止代码重排序:保证代码执行顺序与源代码一致
6. 适用场景
volatile 关键字主要适用于以下场景:
- 多线程共享变量:多个线程同时访问的变量
- 硬件寄存器访问:直接与硬件交互的内存映射寄存器
- 信号处理:在信号处理函数中修改的变量
- 中断处理:在中断处理函数中修改的变量
7. 注意事项
- volatile 不是线程安全的:它只是保证变量的可见性,不保证原子性
- volatile 不能替代锁:对于复合操作(如
i++),仍然需要使用锁或原子操作 - 过度使用 volatile:会降低程序性能,只在必要时使用
8. 代码优化建议
8.1 正确使用 volatile
// 正确:多线程共享的标志变量
volatile bool should_exit = false;
// 错误:复合操作仍需要锁
volatile int counter = 0;
// counter++ 不是原子操作,需要使用锁
8.2 结合其他同步机制
// 结合互斥锁使用
volatile int shared_data = 0;
pthread_mutex_t mutex;
void* thread_func(void* arg) {
// 读取共享数据
int value = shared_data;
// 修改共享数据(需要加锁)
pthread_mutex_lock(&mutex);
shared_data = new_value;
pthread_mutex_unlock(&mutex);
return NULL;
}
9. 总结
volatile 关键字是 C/C++ 中用于确保变量可见性的重要工具,它告诉编译器不要对变量进行某些优化,确保每次使用变量时都从内存中读取最新值。
在多线程环境中,正确使用 volatile 可以避免因编译器优化导致的问题,但它不能替代锁或其他同步机制来保证原子性。
理解 volatile 的工作原理,特别是从汇编层面理解其对编译器优化的影响,有助于我们在编写并发程序时避免常见的陷阱。