You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程环境下std::shared_ptr读写引发double free问题排查

关于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对象)。

我们拆解一下赋值和读取的内部逻辑:

  1. 赋值操作:创建新的shared_ptr → 保存当前shared_ptr的旧指针 → 替换为新指针 → 对旧指针的引用计数减1,若为0则销毁对象。
  2. 读取操作:拷贝当前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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 09:38:22