C语言原子引用计数优化实现正确性问询(适配GCC/Clang)
问题说明
给定以下约束,判断这段针对GCC/Clang编写的引用计数优化代码是否正确,并解答两个问题:
- 这种减少原子操作的优化是否徒劳?
- 不新增原子变量或互斥锁的前提下,能否实现线程安全的同类优化?
约束条件
- 对象传递到其他线程前,必须递增引用计数并将
is_shared设为true; - 当对象引用计数变为1时,必须将
is_shared设为false; Header_is_unique必须100%线程安全。
待分析代码
#include <stdint.h> typedef struct Header { uint8_t is_shared; uint64_t reference_count; } Header; void Header_acquire( Header * const self ) { if (!self->is_shared) { ++self->reference_count; } else { __atomic_fetch_add(&self->reference_count, 1, __ATOMIC_RELAXED); } } uint8_t Header_release( Header * const self ) { if (!self->is_shared) { return !--self->reference_count; } uint64_t const reference_count = __atomic_fetch_sub(&self->reference_count, 1, __ATOMIC_RELEASE); if (reference_count == 2) { __atomic_thread_fence(__ATOMIC_ACQUIRE); self->is_shared = 0; } return reference_count == 1; } uint8_t Header_is_unique( Header const * const self ) { return !self->is_shared && self->reference_count == 1; }
当前代码的问题
这段优化不正确,存在多处线程安全隐患:
1. Header_acquire的竞态条件
当其他线程刚把is_shared从true改为false时,当前线程可能读取到is_shared=0,进而执行普通的++self->reference_count。但此时可能还有其他线程在执行原子递增操作,普通递增和原子操作并发会直接破坏引用计数的正确性,引发数据竞争。
比如线程A完成释放操作,将is_shared设为0;线程B读取到is_shared=0并执行普通递增,同时线程C还在执行原子递增——这会导致引用计数被错误叠加。
2. Header_is_unique的线程安全失效
Header_is_unique直接读取非原子的is_shared和reference_count,完全无法满足“100%线程安全”的约束:
- 两个变量的读取不是原子操作,可能读到不一致的状态(比如
is_shared已被改为0,但reference_count还没被其他线程更新); - 没有内存屏障保证可见性,其他线程的修改可能无法及时被当前线程感知,返回错误的唯一性判断结果。
3. Header_release中is_shared的可见性问题
当引用计数从2减到1时,代码用__ATOMIC_ACQUIRE屏障后修改is_shared=0,但这个屏障无法保证其他线程能及时看到is_shared的修改。其他线程读取is_shared时没有对应的屏障,可能一直读到旧值,要么错误地执行普通操作,要么继续用原子操作浪费性能。
优化是否徒劳?
这种优化思路本身不是徒劳的——在单线程独占对象的场景下,用普通操作替代原子操作确实能降低开销、提升性能。但当前实现没有正确处理多线程下的竞态和可见性问题,导致优化无效,甚至引入严重bug。
不新增原子/锁的前提下,能否实现线程安全的同类优化?
可以,但需要严格调整内存操作顺序和内存屏障的使用,贴合GCC/Clang的内存模型,同时遵守给定约束:
核心修正思路
绑定
is_shared与引用计数的状态一致性:is_shared的状态必须完全由引用计数的原子操作结果决定,不能单独修改。比如:- 当引用计数从1变为2(第一次被共享到其他线程),必须在原子递增后,配合释放屏障设置
is_shared=1; - 当引用计数从2变为1(回到独占状态),必须在原子递减后,配合获取屏障设置
is_shared=0。
- 当引用计数从1变为2(第一次被共享到其他线程),必须在原子递增后,配合释放屏障设置
确保
is_shared读取的可见性:所有读取is_shared的操作,必须在原子读取引用计数之后,或者配合获取屏障,确保读到的是最新状态。修正
Header_is_unique的实现:将两个变量的读取改为原子操作,或者用内存屏障保证读取的原子性和一致性。比如:uint8_t Header_is_unique(Header const *const self) { __atomic_thread_fence(__ATOMIC_ACQUIRE); uint64_t cnt = self->reference_count; uint8_t shared = self->is_shared; return !shared && cnt == 1; }(注:此示例仅为思路,实际需结合引用计数的原子操作顺序调整)
通过以上调整,可以在不新增原子变量或互斥锁的前提下,实现线程安全的原子操作优化,既保留单线程场景下的性能优势,又保证多线程场景的正确性。
内容的提问来源于stack exchange,提问作者João Pires

