C++ std::shared_ptr 线程安全性和最佳实践详解

shared_ptr 线程安全性和最佳实践详解

目录

  1. [shared_ptr 原理和使用](#shared_ptr 原理和使用)
  2. 多线程安全性分析
  3. 常见误区和陷阱
  4. 最佳实践
  5. 总结

shared_ptr 原理和使用

什么是 shared_ptr?

std::shared_ptr 是 C++11 引入的智能指针,用于管理动态分配的对象。它通过引用计数机制实现多个 shared_ptr 实例共享同一个对象的所有权。

内部结构

shared_ptr 的内部结构(简化)如下:

cpp 复制代码
template<typename T>
class shared_ptr {
private:
    T* ptr_;                    // 指向管理的对象
    ControlBlock* ctrl_;        // 控制块(引用计数、弱引用计数等)
    
    struct ControlBlock {
        std::atomic<size_t> ref_count;      // 引用计数(原子操作)
        std::atomic<size_t> weak_count;     // 弱引用计数(原子操作)
        // ... 其他元数据
    };
};

基本使用

cpp 复制代码
#include <memory>

// 创建 shared_ptr
auto ptr1 = std::make_shared<int>(42);
auto ptr2 = ptr1;  // 拷贝,引用计数 +1
auto ptr3 = ptr1;  // 拷贝,引用计数 +1

// 此时引用计数为 3
// 当所有 shared_ptr 都销毁时,对象才会被释放

引用计数机制

cpp 复制代码
{
    auto p1 = std::make_shared<MyClass>();  // ref_count = 1
    {
        auto p2 = p1;  // ref_count = 2
        {
            auto p3 = p1;  // ref_count = 3
        }  // p3 销毁,ref_count = 2
    }  // p2 销毁,ref_count = 1
}  // p1 销毁,ref_count = 0,对象被释放

多线程安全性分析

核心问题:shared_ptr 是线程安全的吗?

简短回答部分线程安全

  • 引用计数操作是线程安全的(原子操作)
  • 同一个 shared_ptr 对象的读写不是线程安全的(需要同步)

1. 引用计数的线程安全性 ✅

多个 shared_ptr 副本可以在不同线程中安全使用:

cpp 复制代码
// ✅ 安全:多个 shared_ptr 副本在不同线程中使用
std::shared_ptr<MyClass> global_ptr = std::make_shared<MyClass>();

// 线程 1
void thread1() {
    auto local_ptr = global_ptr;  // 拷贝,引用计数原子递增
    local_ptr->DoSomething();     // 使用本地副本
}

// 线程 2
void thread2() {
    auto local_ptr = global_ptr;  // 拷贝,引用计数原子递增
    local_ptr->DoSomething();     // 使用本地副本
}

为什么安全?

  • shared_ptr 的拷贝构造函数和析构函数使用原子操作来修改引用计数
  • 多个线程同时拷贝或销毁不同的 shared_ptr 副本是安全的
  • 管理的对象会在最后一个 shared_ptr 销毁时被释放

2. 同一个 shared_ptr 对象的并发访问 ❌

同一个 shared_ptr 对象不能在没有同步的情况下被多个线程同时读写

cpp 复制代码
// ❌ 不安全:多个线程同时读写同一个 shared_ptr 对象
std::shared_ptr<MyClass> shared_ptr_obj = std::make_shared<MyClass>();

// 线程 1:写操作
void thread1() {
    shared_ptr_obj = std::make_shared<MyClass>();  // 写操作
}

// 线程 2:读操作
void thread2() {
    auto ptr = shared_ptr_obj;  // 读操作
    ptr->DoSomething();
}

为什么不安全?

  • shared_ptr 对象本身不是原子的
  • 多个线程同时读写同一个 shared_ptr 对象会导致数据竞争(data race)
  • 可能导致未定义行为

3. 管理的对象的线程安全性

重要shared_ptr 的线程安全性只保证引用计数操作,不保证管理的对象本身的线程安全性

cpp 复制代码
// shared_ptr 本身是安全的,但管理的对象可能不安全
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();

// 线程 1
void thread1() {
    auto local_ptr = ptr;  // ✅ shared_ptr 操作安全
    local_ptr->ModifyData();  // ❌ 如果 MyClass 不是线程安全的,这里不安全
}

// 线程 2
void thread2() {
    auto local_ptr = ptr;  // ✅ shared_ptr 操作安全
    local_ptr->ModifyData();  // ❌ 如果 MyClass 不是线程安全的,这里不安全
}

常见误区和陷阱

误区 1:认为 shared_ptr 完全线程安全

cpp 复制代码
// ❌ 错误理解
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();

// 错误:认为可以安全地并发读写同一个 shared_ptr 对象
void thread1() {
    ptr = std::make_shared<MyClass>();  // 不安全!
}

void thread2() {
    auto p = ptr;  // 不安全!
}

正确做法:使用互斥锁保护

cpp 复制代码
// ✅ 正确做法
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
std::mutex mtx;

void thread1() {
    std::lock_guard<std::mutex> lock(mtx);
    ptr = std::make_shared<MyClass>();  // 安全
}

void thread2() {
    std::lock_guard<std::mutex> lock(mtx);
    auto p = ptr;  // 安全
    // 解锁后使用 p,不需要锁
    p->DoSomething();
}

误区 2:认为管理的对象自动线程安全

cpp 复制代码
// ❌ 错误理解
std::shared_ptr<std::vector<int>> vec = std::make_shared<std::vector<int>>();

// 错误:认为 shared_ptr 保证了 vector 的线程安全
void thread1() {
    auto local_vec = vec;
    local_vec->push_back(1);  // 不安全!vector 不是线程安全的
}

void thread2() {
    auto local_vec = vec;
    local_vec->push_back(2);  // 不安全!
}

正确做法:为管理的对象添加同步机制

cpp 复制代码
// ✅ 正确做法
std::shared_ptr<std::vector<int>> vec = std::make_shared<std::vector<int>>();
std::mutex vec_mtx;

void thread1() {
    auto local_vec = vec;  // shared_ptr 操作安全
    std::lock_guard<std::mutex> lock(vec_mtx);
    local_vec->push_back(1);  // 现在安全了
}

误区 3:在析构函数中直接访问 shared_ptr 成员

cpp 复制代码
// ❌ 可能不安全
class MyClass {
    std::shared_ptr<OtherClass> member_ptr_;
    
public:
    ~MyClass() {
        if (member_ptr_ != nullptr) {  // 可能崩溃!
            member_ptr_->DoSomething();
        }
    }
};

问题 :在析构过程中,member_ptr_ 可能正在析构,直接访问可能不安全。

正确做法:先创建本地副本

cpp 复制代码
// ✅ 安全做法
class MyClass {
    std::shared_ptr<OtherClass> member_ptr_;
    
public:
    ~MyClass() {
        auto local_ptr = member_ptr_;  // 创建本地副本
        if (local_ptr != nullptr) {
            local_ptr->DoSomething();  // 安全
        }
    }
};

最佳实践

1. 多线程环境下的 shared_ptr 使用

✅ 推荐做法:每个线程使用自己的副本
cpp 复制代码
class ThreadSafeManager {
private:
    std::shared_ptr<Resource> resource_;
    std::mutex mtx_;  // 保护 resource_ 的读写
    
public:
    std::shared_ptr<Resource> GetResource() {
        std::lock_guard<std::mutex> lock(mtx_);
        return resource_;  // 返回副本,解锁后使用
    }
    
    void SetResource(std::shared_ptr<Resource> new_resource) {
        std::lock_guard<std::mutex> lock(mtx_);
        resource_ = new_resource;
    }
};

// 使用
void worker_thread(ThreadSafeManager& manager) {
    auto resource = manager.GetResource();  // 获取副本
    // 解锁后使用 resource,不需要锁
    resource->DoWork();
}
✅ 推荐做法:使用 atomic<shared_ptr>(C++20)
cpp 复制代码
#include <atomic>
#include <memory>

// C++20 提供了 std::atomic<std::shared_ptr<T>>
std::atomic<std::shared_ptr<MyClass>> atomic_ptr;

// 线程 1
void thread1() {
    atomic_ptr.store(std::make_shared<MyClass>());  // 原子操作
}

// 线程 2
void thread2() {
    auto ptr = atomic_ptr.load();  // 原子操作
    ptr->DoSomething();
}

注意std::atomic<std::shared_ptr<T>> 只在 C++20 及以后版本可用。

2. 析构函数中的 shared_ptr 使用

✅ 推荐做法:先创建本地副本
cpp 复制代码
class MyClass {
    std::shared_ptr<Dependency> dependency_;
    
public:
    ~MyClass() {
        // 先创建本地副本,确保安全
        auto dep = dependency_;
        if (dep != nullptr) {
            dep->Cleanup();
        }
    }
};
✅ 推荐做法:多层 shared_ptr 访问
cpp 复制代码
class MyClass {
    std::shared_ptr<Manager> manager_;
    
public:
    ~MyClass() {
        // 多层访问,每一层都创建副本
        auto mgr = manager_;
        if (mgr != nullptr) {
            auto handler = mgr->GetHandler();
            if (handler != nullptr) {
                handler->Cleanup();
            }
        }
    }
};

3. 避免循环引用

cpp 复制代码
// ❌ 循环引用导致内存泄漏
class Parent {
    std::shared_ptr<Child> child_;
};

class Child {
    std::shared_ptr<Parent> parent_;  // 循环引用!
};

// 使用
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
parent->child_ = child;
child->parent_ = parent;  // 循环引用,无法释放

解决方案 :使用 weak_ptr 打破循环

cpp 复制代码
// ✅ 使用 weak_ptr 打破循环
class Parent {
    std::shared_ptr<Child> child_;
};

class Child {
    std::weak_ptr<Parent> parent_;  // 使用 weak_ptr
};

// 使用
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
parent->child_ = child;
child->parent_ = parent;  // weak_ptr 不会增加引用计数

// parent 和 child 可以正常释放

4. 性能考虑

✅ 推荐:使用 make_shared
cpp 复制代码
// ✅ 推荐:一次内存分配
auto ptr = std::make_shared<MyClass>(arg1, arg2);

// ❌ 不推荐:两次内存分配
auto ptr = std::shared_ptr<MyClass>(new MyClass(arg1, arg2));

优势

  • make_shared 将对象和控制块分配在同一块内存中
  • 减少内存分配次数,提高性能
  • 更好的异常安全性
✅ 推荐:避免不必要的拷贝
cpp 复制代码
// ❌ 不必要的拷贝
void Process(std::shared_ptr<Data> data) {
    // 如果不需要修改 data,使用 const&
}

// ✅ 推荐:按需选择参数类型
void Process(const std::shared_ptr<Data>& data) {
    // 不需要拷贝,直接使用引用
}

// 或者
void Process(std::shared_ptr<Data> data) {
    // 如果需要延长生命周期,使用值传递
}

5. 线程安全的单例模式

cpp 复制代码
class Singleton {
private:
    static std::shared_ptr<Singleton> instance_;
    static std::mutex mtx_;
    
    Singleton() = default;
    
public:
    static std::shared_ptr<Singleton> GetInstance() {
        // 双重检查锁定
        if (instance_ == nullptr) {
            std::lock_guard<std::mutex> lock(mtx_);
            if (instance_ == nullptr) {
                instance_ = std::make_shared<Singleton>();
            }
        }
        return instance_;  // 返回副本,线程安全
    }
};

std::shared_ptr<Singleton> Singleton::instance_ = nullptr;
std::mutex Singleton::mtx_;

总结

shared_ptr 线程安全性要点

  1. 引用计数操作是线程安全的

    • 多个 shared_ptr 副本可以在不同线程中安全使用
    • 拷贝和销毁操作使用原子操作
  2. 同一个 shared_ptr 对象不是线程安全的

    • 多个线程同时读写同一个 shared_ptr 对象需要同步
    • 使用互斥锁或 atomic<shared_ptr>(C++20)
  3. 管理的对象的线程安全性是独立的

    • shared_ptr 只保证引用计数的线程安全
    • 管理的对象本身需要额外的同步机制

最佳实践总结

  1. 多线程环境 :每个线程使用自己的 shared_ptr 副本
  2. 析构函数:先创建本地副本再使用
  3. 避免循环引用 :使用 weak_ptr 打破循环
  4. 性能优化 :使用 make_shared,避免不必要的拷贝
  5. 同步机制 :需要时使用互斥锁保护 shared_ptr 对象

关键原则

记住shared_ptr 的线程安全性只保证引用计数操作,不保证对象本身的线程安全性,也不保证同一个 shared_ptr 对象的并发访问安全。

遵循这些原则和最佳实践,可以安全、高效地使用 shared_ptr 进行多线程编程。

相关推荐
星期天21 小时前
【无标题】
数据结构·c++·算法
E***U9452 小时前
Kotlin注解处理器
java·开发语言·kotlin
せいしゅん青春之我2 小时前
【JavaEE进阶】JVM-面试中的高频考点1
java·网络·jvm·笔记·面试·java-ee
老李四2 小时前
Java 内存分配与回收策略
java·jvm·算法
陈逸轩*^_^*2 小时前
深入理解 Java JVM,包括垃圾收集器原理、垃圾回收算法原理、类加载机制等
java·jvm
2***57422 小时前
Java内存泄漏排查工具
java·开发语言
S***H2832 小时前
Java在微服务网关中的实现
java·开发语言·微服务
cherry有点甜·2 小时前
标题调用外部接口apifox与浏览器显示不一致
java
家有两宝,感恩遇见2 小时前
不能明文传证件号码后端加密解密最简单的方式AES
java·服务器·开发语言