咨询:无互斥锁下引用计数竞态条件的规避方案(线程A/B场景)
嘿,这个场景其实是无锁并发里典型的「延迟删除」+「原子引用计数」的应用场景,完全可以不用 mutex,靠原子操作和内存栅栏(atomic_thread_fence)来解决。我给你拆解下思路和具体实现:
核心思路
问题的本质是要避免线程A在datablock内存被释放后,还去执行AddRef操作。要做到这一点,我们需要:
- 用原子类型管理datablock的全局指针和内部引用计数,保证并发访问的原子性和可见性。
- 借助内存栅栏/原子操作的内存顺序建立线程间的操作顺序约束,避免指令重排序导致的野指针访问。
- 采用延迟删除策略:线程B不能直接删除datablock,必须等所有持有引用的线程都用完后再释放内存。
具体实现(以C++为例)
首先定义datablock结构和全局原子指针:
#include <atomic> struct DataBlock { std::atomic<int> ref_count; // 原子引用计数,初始值设为1(假设创建时默认持有一个引用) // 你的其他数据成员 }; std::atomic<DataBlock*> global_data_block = nullptr; // 全局共享的datablock指针
线程A的操作逻辑
线程A获取datablock并执行AddRef的流程:
void thread_a() { // 原子加载全局指针,用memory_order_acquire确保能看到线程B之前的所有写操作 DataBlock* block = global_data_block.load(std::memory_order_acquire); if (block != nullptr) { // 显式内存栅栏:确保后续的AddRef操作不会被重排序到指针加载之前 std::atomic_thread_fence(std::memory_order_acquire); // 执行AddRef:原子递增引用计数,用memory_order_relaxed即可(已有栅栏保证顺序) block->ref_count.fetch_add(1, std::memory_order_relaxed); // 这里可以安全使用datablock了 // ... 你的业务逻辑 ... // 使用完毕后释放引用 if (block->ref_count.fetch_sub(1, std::memory_order_release) == 1) { // 最后一个引用被释放,删除datablock delete block; } } }
线程B的操作逻辑
线程B释放/删除datablock的流程:
void thread_b() { // 原子交换全局指针为nullptr,用memory_order_acq_rel保证操作的原子性和顺序约束 DataBlock* block_to_delete = global_data_block.exchange(nullptr, std::memory_order_acq_rel); if (block_to_delete != nullptr) { // 显式内存栅栏:确保所有对datablock的访问操作都已完成,再处理引用计数 std::atomic_thread_fence(std::memory_order_release); // 递减引用计数,如果递减后为1,说明之前只有当前线程持有引用 if (block_to_delete->ref_count.fetch_sub(1, std::memory_order_release) == 1) { // 没有其他线程持有引用,安全删除 delete block_to_delete; } // 如果递减后大于1,说明还有线程A持有引用,由线程A在最后释放时删除 } }
关键细节解释
- 内存顺序与栅栏的作用:
- 线程A的
load用memory_order_acquire,确保线程B之前对全局指针的修改(置空)能被线程A看到。 - 显式的
atomic_thread_fence避免了编译器/CPU的指令重排序,保证线程A一定是先确认指针有效,再执行AddRef,不会出现“先递增引用计数,再发现指针已被置空”的情况。 - 线程B的
exchange用memory_order_acq_rel,确保置空指针的操作对所有线程可见,且后续的引用计数操作不会被重排序到置空之前。
- 线程A的
- 延迟删除的必要性:线程B不会直接删除datablock,而是把全局指针置空,让后续线程无法获取它,然后递减引用计数。如果还有线程A持有引用,引用计数会大于1,线程B就不删除,等线程A用完后递减到0时再删除,从根本上避免了线程A访问已释放内存的问题。
注意事项
- 所有访问datablock的线程必须严格遵循这个流程,不能有线程直接绕过原子指针加载和引用计数操作去访问datablock。
- 内存顺序的选择不能随意,错误的内存顺序会导致可见性问题或重排序问题,比如用
memory_order_relaxed加载指针可能会看不到线程B的置空操作。
内容的提问来源于stack exchange,提问作者Patrick Jeeves
相关产品推荐
相关产品推荐

