You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

编程中原子操作的适用场景:单读写线程能否省略原子操作?

单读单写线程场景下是否可以省略原子操作?

问题背景

已知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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 19:25:00