C++跨进程能否共享atomic_int64_t原子变量及实现示例
问题1答案:跨进程场景下C++原子变量可以保证原子性
你的推测基本正确,但有几个细节需要明确:
- C++内存模型的并发规则不区分「同进程内的多线程访问」和「不同进程的访问」:只要多个执行流访问的是同一块物理内存,CPU级别的原子操作保障(缓存行锁定、总线信号锁定)对所有核心上运行的所有执行流生效,不管执行流属于哪个进程。
- 这个结论成立的前提有两个:
- 你用的原子类型是无锁实现的。如果原子类型内部用了mutex锁来实现(部分平台上超过自然字长的原子类型会这么做),绝对不能跨进程用——mutex是进程内核对象,存储的是进程本地的状态,跨进程访问会直接触发未定义行为。
std::atomic_uint64_t在x86-64、ARM64等所有主流64位平台上都是默认无锁的,保险起见可以在代码里调用is_lock_free()校验。 - 原子对象在共享内存中的内存布局、对齐要求在两个进程中完全一致(同一套编译参数、同架构下基本不会有问题)。
- 你用的原子类型是无锁实现的。如果原子类型内部用了mutex锁来实现(部分平台上超过自然字长的原子类型会这么做),绝对不能跨进程用——mutex是进程内核对象,存储的是进程本地的状态,跨进程访问会直接触发未定义行为。
- C20标准正式明确了共享内存中原子对象的行为规范,C20之前的版本属于实现定义行为,但GCC、Clang、MSVC等主流编译器在主流硬件架构上的实现都已经支持跨进程原子操作,已经在生产环境验证多年。
问题2答案:跨进程共享原子变量的实现方式
核心逻辑很简单:把原子对象和要共享的数组一起放在共享内存区域,两个进程都映射这块共享内存到自身地址空间,仅在第一次创建共享内存时初始化原子对象,后续直接通过原子的load/store接口做同步即可。
关键注意事项
- 原子对象只能初始化一次:第一个创建共享内存的进程用
placement new在共享内存地址上构造原子对象,后续打开共享内存的进程直接强转指针使用,不要重复构造,否则会覆盖原子状态。 - 选对内存序:生产者进程(写数组的A进程)写完所有数组数据后,用
std::memory_order_release语义写入有效长度;消费者进程(读数据的B进程)用std::memory_order_acquire语义读取长度,保证读到长度值时,之前写入的所有数组数据都对B进程可见,不会读到脏数据。 - 不要在共享内存中存储进程本地的虚拟地址,跨进程映射后虚拟地址不通用,会触发野指针。
可运行代码示例(POSIX平台,Linux/macOS适用)
公共定义
两个进程共用同一份共享内存布局定义:
#include <atomic> #include <cstdint> #include <unistd.h> #include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #include <cstring> #include <iostream> // 共享内存结构体,注意原子变量放在开头保证自然对齐 struct SharedBuffer { std::atomic_uint64_t valid_count; // 数组有效元素个数 int data[1024]; // 实际存储数据的数组,长度可按需调整 }; constexpr const char* SHM_PATH = "/demo_shm_atomic"; constexpr size_t SHM_BYTES = sizeof(SharedBuffer);
进程A(写进程)代码
int main() { // 创建共享内存段 int shm_fd = shm_open(SHM_PATH, O_CREAT | O_RDWR, 0666); if (shm_fd == -1) { perror("shm_open fail"); return 1; } if (ftruncate(shm_fd, SHM_BYTES) == -1) { perror("ftruncate fail"); shm_unlink(SHM_PATH); close(shm_fd); return 1; } // 映射共享内存到当前进程地址空间 void* shm_addr = mmap(nullptr, SHM_BYTES, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); if (shm_addr == MAP_FAILED) { perror("mmap fail"); close(shm_fd); shm_unlink(SHM_PATH); return 1; } close(shm_fd); // 第一次创建,用placement new构造共享内存里的对象,初始化有效个数为0 SharedBuffer* buffer = new (shm_addr) SharedBuffer{}; // 校验原子类型是否无锁,必须做 if (!buffer->valid_count.is_lock_free()) { std::cerr << "atomic_uint64_t is not lock-free on this platform, cross-process use is invalid" << std::endl; munmap(shm_addr, SHM_BYTES); shm_unlink(SHM_PATH); return 1; } buffer->valid_count.store(0, std::memory_order_relaxed); // 模拟写入数组数据:写入123个元素 for (int i = 0; i < 123; ++i) { buffer->data[i] = i * 3; } // 写完所有数据后,用release语义更新有效长度,保证前面的写入对读进程可见 buffer->valid_count.store(123, std::memory_order_release); std::cout << "[Process A] wrote 123 elements, updated valid count" << std::endl; // 停留10秒方便读进程读取,实际业务按自身生命周期管理共享内存 sleep(10); // 资源清理 buffer->~SharedBuffer(); munmap(shm_addr, SHM_BYTES); shm_unlink(SHM_PATH); return 0; }
进程B(读进程)代码
int main() { // 打开已存在的共享内存段 int shm_fd = shm_open(SHM_PATH, O_RDWR, 0666); if (shm_fd == -1) { perror("shm_open fail, please start write process first"); return 1; } // 映射共享内存 void* shm_addr = mmap(nullptr, SHM_BYTES, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); if (shm_addr == MAP_FAILED) { perror("mmap fail"); close(shm_fd); return 1; } close(shm_fd); SharedBuffer* buffer = static_cast<SharedBuffer*>(shm_addr); // 轮询等待有效长度更新,用acquire语义读取保证数据可见 uint64_t count = 0; while ((count = buffer->valid_count.load(std::memory_order_acquire)) == 0) { usleep(1000); // 短暂让出CPU避免空转占满资源 } std::cout << "[Process B] got valid count: " << count << std::endl; // 读取并打印数组内容 for (uint64_t i = 0; i < count; ++i) { std::cout << "data[" << i << "] = " << buffer->data[i] << std::endl; } // 清理 munmap(shm_addr, SHM_BYTES); return 0; }
编译运行说明
- 编译时需要链接POSIX实时库和线程库,编译命令示例:
g++ process_a.cpp -o process_a -lrt -pthreadg++ process_b.cpp -o process_b -lrt -pthread - 先启动
process_a,再启动process_b即可看到读进程正确拿到长度和数据。 - Windows平台只需要把
shm_open/mmap这套POSIX接口替换成CreateFileMapping/MapViewOfFile即可,原子变量的操作逻辑完全一致。
内容的提问来源于stack exchange,提问作者54138qqqq
相关产品推荐
相关产品推荐

