编程中原子操作的适用场景:单读写线程能否省略原子操作?
问题背景
已知CPU提供原子指令用于原子性访问指定内存地址,但对原子指令的适用场景存在疑惑:有专家指出“当超过两个线程访问共享变量时应使用原子操作”,但想明确单读单写线程访问共享变量的情况——是否可以省略原子操作?
非原子代码示例
以下是单读单写的非原子实现代码:
#define READY 1 #define NOTREADY 0 int flag; int msg; // 读线程 void reader() { while (flag == NOTREADY) ; // 非原子访问 process(msg); flag = NOTREADY; // 非原子访问 } // 写线程 void writer() { while (1) { while (flag == READY) ; msg = send_msg(); flag = READY; // 非原子访问 } }
逻辑说明:读线程循环等待flag变为READY后处理msg,再将flag重置为NOTREADY;写线程循环等待flag为NOTREADY后赋值msg,再设置flag为READY。
测试代码及现象
我运行了如下测试代码,结果看似正常:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #define READY 1 #define NOTREADY 0 int msg, flag = NOTREADY, cnt = 0; void process(int msg) { printf("Processing msg: %d\n", msg); } void *reader() { while (cnt < 200000) { while (flag == NOTREADY) ; process(msg); flag = NOTREADY; } } int send_msg() { cnt ++; printf("Sending msg: %d\n", cnt); return cnt; } void *writer() { while (cnt < 200000) { while (flag == READY) ; msg = send_msg(); flag = READY; } } int main() { pthread_t r_id, w_id; pthread_create(&r_id, NULL, reader, NULL); pthread_create(&w_id, NULL, writer, NULL); pthread_join(r_id, NULL); pthread_join(w_id, NULL); return 0; }
核心疑问
当程序仅有一个读线程和一个写线程访问共享变量时,是否可以省略原子操作?若不能,原因是什么?
回答
即使是单读单写线程,也不能随意省略原子操作(或等价的内存同步机制),核心原因涉及以下几点:
1. 编译器优化导致的可见性问题
普通变量的读写没有内存同步语义,编译器会基于“线程内代码无副作用”的假设做优化。比如读线程中的while (flag == NOTREADY),编译器可能会将flag的值缓存到寄存器中,不再从内存重新读取——这会导致读线程永远看不到写线程对flag的修改,直接陷入死循环。
你当前的测试代码能正常运行,是因为printf内部包含锁操作,相当于隐式插入了内存屏障,强制刷新了CPU缓存,让flag的修改能被读线程感知。如果去掉printf、开启更高优化级别(如-O2),死循环问题会立刻暴露。
2. 硬件/编译器的指令重排问题
写线程中msg = send_msg();和flag = READY;的执行顺序,编译器或CPU可能会进行重排——也就是说,flag被设置为READY的操作可能先于msg的赋值完成。这会导致读线程看到flag为READY时,读到的是msg的旧值,出现逻辑错误。
普通变量的读写没有内存屏障约束,无法保证操作的顺序性,这种重排是完全符合C标准的未定义行为。
3. 原子性的潜在风险
虽然大多数现代CPU对int类型的单个读写指令是原子的,但C标准并没有保证普通变量的读写是原子的。如果变量大小超过CPU字长(比如32位CPU上的64位变量),普通读写会被拆分为多个指令,必然是非原子的,可能出现读写撕裂的问题。
正确的做法
针对单读单写的场景,推荐两种可靠方案:
- 使用C标准的原子类型:将
flag和msg声明为_Atomic int,既保证原子性,又提供可见性和内存顺序约束。 - 使用互斥锁:通过
pthread_mutex_t对共享变量的访问加锁,虽然开销略高,但逻辑更直观,也能避免所有线程同步问题。
内容的提问来源于stack exchange,提问作者Teng Wu

