C11原子操作能否替代互斥锁?共享字符串读写场景实操疑问
原子操作适用场景及你的问题解决方案
你的示例的无锁实现方案
你遇到的问题本质不是原子操作没用,是你只保证了指针本身读写的原子性,没有处理指针指向内存的生命周期同步,单写多读的场景完全可以用原子操作实现,不需要互斥锁,这里给一个适合嵌入式场景的seqlock双缓冲方案,避免动态内存风险:
#include <string.h> #define CONFIGURED_MAX 128 // 双缓冲,预分配避免动态内存问题 static char str_buf[2][CONFIGURED_MAX]; // 原子版本号:奇数表示正在写入,偶数表示写入完成 static _Atomic unsigned int seq = 0; // 原子标记当前可用的缓冲索引 static _Atomic int current_buf = 0; void threadA() { char some[] = "Some String we got over network somehow"; int next_buf = 1 - current_buf; // 写入空闲缓冲 // 标记开始写入,版本号变奇数 __atomic_add_fetch(&seq, 1, __ATOMIC_RELEASE); // 写入空闲缓冲,不影响正在读的线程 strncpy(str_buf[next_buf], some, CONFIGURED_MAX - 1); str_buf[next_buf][CONFIGURED_MAX - 1] = '\0'; // 交换当前可用缓冲 __atomic_store(¤t_buf, next_buf, __ATOMIC_RELEASE); // 标记写入完成,版本号变偶数 __atomic_add_fetch(&seq, 1, __ATOMIC_RELEASE); } void cpy_that_char(char* dst) { unsigned int seq_v; int buf_idx; do { // 读取版本号,如果是奇数说明正在写入,重试 while ((seq_v = __atomic_load(&seq, __ATOMIC_ACQUIRE)) % 2 != 0); // 读当前缓冲索引 buf_idx = __atomic_load(¤t_buf, __ATOMIC_ACQUIRE); // 复制内容 strncpy(dst, str_buf[buf_idx], CONFIGURED_MAX); // 检查版本号是否变化,变化说明复制过程中有写入,重试 } while (__atomic_load(&seq, __ATOMIC_ACQUIRE) != seq_v); } void threadB() { char grabbed_str[CONFIGURED_MAX]; cpy_that_char(grabbed_str); // 直接使用grabbed_str即可,没有生命周期问题 }
这个方案完全没有锁竞争,也不存在内存释放导致的野指针问题,非常适合嵌入式实时场景。
原子操作的常见真实使用场景
原子操作的核心优势是没有上下文切换开销、不会产生死锁,适合临界区非常短、对性能/实时性要求高的场景,常见使用场景包括:
- 轻量级统计计数器:比如多线程统计中断触发次数、网络报文收发量,直接用
__atomic_add_fetch/__atomic_sub_fetch操作即可,比互斥锁性能高一个数量级 - 全局状态标记:比如系统运行状态(RUN/STOP/ERROR)、功能开关标记,只有少数几个固定值,单线程修改、多线程读取,原子读写完全满足需求,不需要加锁
- 无锁数据结构实现:比如无锁队列、无锁栈的入队出队操作,用原子CAS(比较并交换)操作实现节点指针的更新,吞吐远高于锁实现的队列,适合嵌入式实时通信场景
- 引用计数管理:比如共享资源的引用计数增减,用原子操作保证多线程下计数正确,不需要每次访问资源都加锁
需要注意的是,原子操作不是银弹,当你需要保护的临界区逻辑非常复杂,无法通过无锁数据结构的设计拆分到单个或多个有序的原子操作覆盖时,用互斥锁是更合理的选择,不需要强行用原子操作。
内容的提问来源于stack exchange,提问作者glades
相关产品推荐
相关产品推荐

