多线程环境下std::shared_ptr读写引发double free问题排查
嘿,这个坑我之前也踩过!很多人都会误以为std::shared_ptr是完全线程安全的,但其实这里有个非常关键的认知误区——它的线程安全是有边界的,不是你想的那种“随便怎么用都安全”。
先明确std::shared_ptr的线程安全范围
std::shared_ptr的线程安全只保证引用计数的增减操作是原子的:
- 多个线程同时拷贝同一个
shared_ptr(也就是增加引用计数),没问题; - 多个线程同时销毁各自持有的
shared_ptr实例(也就是减少引用计数),也没问题。
但!对同一个shared_ptr对象的并发读写操作是完全不安全的——比如一个线程给这个shared_ptr赋值(替换它指向的对象),另一个线程同时读取它,这就会产生数据竞争,进而导致你遇到的double free。
结合你的场景分析原因
你的场景是:全局数组里的shared_ptr,一个线程替换元素(本质是给数组里的某个shared_ptr对象赋值),另一个线程读取该元素(本质是拷贝这个shared_ptr对象)。
我们拆解一下赋值和读取的内部逻辑:
- 赋值操作:创建新的
shared_ptr→ 保存当前shared_ptr的旧指针 → 替换为新指针 → 对旧指针的引用计数减1,若为0则销毁对象。 - 读取操作:拷贝当前
shared_ptr的指针 → 对该指针的引用计数加1。
如果这两个操作同时发生,就可能出现这种致命情况:
读取线程刚拷贝了旧指针,但还没来得及给它的引用计数加1,赋值线程已经把旧指针的引用计数减到0并销毁了对象。这时候读取线程手里的shared_ptr指向的是已经被销毁的对象,当这个拷贝出来的shared_ptr生命周期结束时,就会尝试再次释放这个已经被释放的对象——这就是AddressSanitizer提示的double free。
解决方案
有两种靠谱的方式解决这个问题:
1. 手动加锁保护数组访问
给全局数组的读写操作加个互斥锁,确保同一时间只有一个线程能访问数组里的shared_ptr对象:
#include <mutex> #include <array> #include <memory> std::mutex array_access_mutex; std::array<std::shared_ptr<YourObj>, 10> global_array; // 写入线程的代码 void replace_element(int idx, std::shared_ptr<YourObj> new_obj) { std::lock_guard<std::mutex> lock(array_access_mutex); global_array[idx] = std::move(new_obj); } // 读取线程的代码 std::shared_ptr<YourObj> get_element(int idx) { std::lock_guard<std::mutex> lock(array_access_mutex); return global_array[idx]; }
这样读取操作会完整拷贝到一个本地的shared_ptr,之后你可以安全地使用这个本地实例,不需要再持有锁。
2. 使用C++20的std::atomicstd::shared_ptr
如果你的项目支持C++20及以上,可以直接用std::atomic<std::shared_ptr>,它专门实现了对shared_ptr的原子读写操作,不需要手动加锁:
#include <atomic> #include <array> #include <memory> std::array<std::atomic<std::shared_ptr<YourObj>>, 10> global_array; // 写入操作 void replace_element(int idx, std::shared_ptr<YourObj> new_obj) { global_array[idx].store(std::move(new_obj), std::memory_order_release); } // 读取操作 std::shared_ptr<YourObj> get_element(int idx) { return global_array[idx].load(std::memory_order_acquire); }
std::memory_order_release和std::memory_order_acquire是内存屏障,确保读写操作的可见性,避免出现内存乱序的问题。
总结
std::shared_ptr的线程安全只覆盖引用计数的原子操作,不包括对shared_ptr对象本身的并发读写。你的问题核心就是没有保护全局数组中shared_ptr对象的并发访问,导致了数据竞争和double free。只要给读写操作加锁或者用原子shared_ptr,就能解决这个问题。
内容的提问来源于stack exchange,提问作者Oleg

