首页 > 后端开发 > C++ > 正文

C++如何在多线程中安全使用shared_ptr

P粉602998670
发布: 2025-09-10 08:17:01
原创
463人浏览过
shared_ptr的引用计数线程安全,但所指对象的访问需额外同步。

c++如何在多线程中安全使用shared_ptr

shared_ptr
登录后复制
在多线程中使用时,其内部的引用计数操作是原子且线程安全的,但它所指向的实际数据(managed object)的访问并非自动线程安全。因此,对共享数据的修改必须通过互斥锁(如
std::mutex
登录后复制
)等同步机制来保护。

解决方案

在我看来,理解

shared_ptr
登录后复制
在多线程环境下的安全性,首先要区分两个层面:
shared_ptr
登录后复制
自身的管理(即引用计数)和它所管理的实际对象的数据。
shared_ptr
登录后复制
的设计者们非常周到,确保了其内部的引用计数增减操作是原子性的。这意味着,多个线程可以同时对同一个
shared_ptr
登录后复制
实例进行复制、赋值或销毁操作,而不会导致引用计数器损坏,从而避免了内存泄漏或过早释放的问题。这是它在多线程中能够“安全”使用的基础。

然而,这种安全性仅限于

shared_ptr
登录后复制
自身这个“智能指针”的层面。一旦你通过
shared_ptr
登录后复制
获取到它所指向的实际对象(
*ptr
登录后复制
ptr->member
登录后复制
),并试图修改这个对象的数据时,
shared_ptr
登录后复制
就无能为力了。它并不知道你的对象内部有什么数据,更不会为你的数据访问提供任何同步保护。所以,如果你有多个线程共享同一个
shared_ptr
登录后复制
,并且这些线程都会对
shared_ptr
登录后复制
指向的对象进行写操作,那么数据竞争(data race)是必然会发生的。这就是为什么我们说,
shared_ptr
登录后复制
是线程安全的,但它所管理的数据不是。

要真正安全地在多线程中使用

shared_ptr
登录后复制
所指向的数据,核心策略就是对数据访问进行外部同步。最直接、最常用的方法就是使用互斥锁(
std::mutex
登录后复制
)。每当有线程需要读取或修改共享数据时,它必须先获取到对应的锁,操作完成后再释放锁。这确保了在任何给定时间,只有一个线程能够访问关键数据区域,从而避免了数据竞争。当然,这只是最基础的同步方式,根据具体场景,还可以考虑其他更高级的同步原语,比如读写锁(
std::shared_mutex
登录后复制
)来优化读多写少的场景,或者使用原子类型来处理简单的共享变量。

立即学习C++免费学习笔记(深入)”;

shared_ptr
登录后复制
的引用计数为何是线程安全的,但其管理的数据却不是?

这个问题其实触及了C++标准库设计哲学的一个核心点:提供工具,而不是强制行为。

shared_ptr
登录后复制
的引用计数器(通常存储在一个独立的“控制块”中)的增减操作被设计为原子性的,这通常通过底层硬件指令或
std::atomic
登录后复制
类型来实现。例如,在一个典型的实现中,当一个
shared_ptr
登录后复制
被复制时,它会原子地递增控制块中的引用计数;当它被销毁时,会原子地递减。这种原子性保证了即使多个线程同时创建或销毁指向同一对象的
shared_ptr
登录后复制
副本,引用计数也能正确更新,从而确保对象在所有引用都消失后才被销毁。这是非常重要的,因为它防止了“悬空指针”或“过早释放”的内存管理问题,而这些问题在没有智能指针的裸指针多线程场景中极其常见且难以调试。

然而,

shared_ptr
登录后复制
对于它所指向的实际数据是“无知”的。标准库无法预知你存储在
shared_ptr
登录后复制
中的对象是简单的
int
登录后复制
,还是复杂的自定义类,更无法知道你的自定义类内部有哪些成员,以及这些成员如何被访问和修改。如果
shared_ptr
登录后复制
要为它管理的所有数据都提供自动同步,那将是一个巨大的性能开销,并且会限制其通用性。比如,如果你的数据是不可变的(immutable),那么根本不需要锁。如果
shared_ptr
登录后复制
强行加锁,那就会造成不必要的性能浪费。所以,标准库将数据本身的同步责任留给了程序员。这种设计理念是“只为你需要的功能付费”(pay for what you use),它赋予了开发者更大的灵活性和控制权,但也要求开发者对多线程编程有更深入的理解。在我看来,这是C++在性能和抽象之间寻求平衡的典型体现。

AI-Text-Classifier
AI-Text-Classifier

OpenAI官方出品,可以区分人工智能书写的文本和人类书写的文本

AI-Text-Classifier 59
查看详情 AI-Text-Classifier

如何在多线程环境中正确地保护
shared_ptr
登录后复制
所指向的数据?

正确保护

shared_ptr
登录后复制
所指向的数据,是多线程编程中一个关键且需要细致思考的环节。这里有几种常见且有效的策略:

  1. 使用互斥锁(

    std::mutex
    登录后复制
    )进行显式同步: 这是最直接、最通用的方法。当你需要访问或修改
    shared_ptr
    登录后复制
    指向的对象时,使用
    std::mutex
    登录后复制
    来保护这个操作。

    #include <iostream>
    #include <memory>
    #include <thread>
    #include <mutex>
    #include <vector>
    
    class MyData {
    public:
        int value;
        MyData(int v = 0) : value(v) {}
        void increment() { value++; }
    };
    
    std::shared_ptr<MyData> global_data = std::make_shared<MyData>(0);
    std::mutex data_mutex;
    
    void worker_function() {
        for (int i = 0; i < 10000; ++i) {
            std::lock_guard<std::mutex> lock(data_mutex); // 保护数据访问
            global_data->increment();
        }
    }
    
    // int main() {
    //     std::vector<std::thread> threads;
    //     for (int i = 0; i < 4; ++i) {
    //         threads.emplace_back(worker_function);
    //     }
    //     for (auto& t : threads) {
    //         t.join();
    //     }
    //     std::cout << "Final value: " << global_data->value << std::endl; // 应该接近 40000
    //     return 0;
    // }
    登录后复制

    这种方式确保了在任何时刻,只有一个线程能够修改

    global_data->value
    登录后复制
    std::lock_guard
    登录后复制
    是一个RAII(资源获取即初始化)封装,它在构造时加锁,在析构时自动解锁,避免了忘记解锁的常见错误。

  2. 采用不可变数据(Immutable Data)策略: 如果你的数据对象在创建后就不会再被修改,那么它就是天然线程安全的。多个线程可以自由地读取它,而无需任何锁。这是一种非常强大的并发模式,因为它完全消除了数据竞争的可能性,并且通常能带来更好的性能。当需要“修改”数据时,实际上是创建一个新的、修改后的数据副本,然后更新

    shared_ptr
    登录后复制
    去指向这个新副本。

    #include <iostream>
    #include <memory>
    #include <thread>
    #include <mutex>
    #include <vector>
    
    class ImmutableData {
    public:
        const int value;
        ImmutableData(int v) : value(v) {}
        // 没有修改成员的方法
    };
    
    std::shared_ptr<const ImmutableData> current_data = std::make_shared<const ImmutableData>(0);
    std::mutex update_mutex; // 保护指针本身的更新
    
    void updater_function() {
        for (int i = 0; i < 1000; ++i) {
            std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模拟一些工作
            std::lock_guard<std::mutex> lock(update_mutex);
            current_data = std::make_shared<const ImmutableData>(current_data->value + 1); // 创建新对象并更新指针
        }
    }
    
    void reader_function() {
        for (int i = 0; i < 1000; ++i) {
            std::shared_ptr<const ImmutableData> local_copy;
            {
                std::lock_guard<std::mutex> lock(update_mutex); // 保护指针读取
                local_copy = current_data; // 获取当前指针的副本
            }
            // 现在可以安全地读取local_copy指向的数据,因为它是不可变的
            // std::cout << "Read value: " << local_copy->value << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }
    }
    
    // int main() {
    //     std::vector<std::thread> threads;
    //     threads.emplace_back(updater_function);
    //     for (int i = 0; i < 3; ++i) {
    //         threads.emplace_back(reader_function);
    //     }
    //     for (auto& t : threads) {
    //         t.join();
    //     }
    //     std::cout << "Final value: " << current_data->value << std::endl;
    //     return 0;
    // }
    登录后复制

    需要注意的是,虽然数据本身是不可变的,但

    shared_ptr
    登录后复制
    指针的更新(从旧对象指向新对象)仍然需要同步保护。

  3. 使用

    std::atomic<std::shared_ptr<T>>
    登录后复制
    原子化指针更新: C++20引入了
    std::atomic<std::shared_ptr<T>>
    登录后复制
    ,它使得
    shared_ptr
    登录后复制
    本身的复制和赋值操作成为原子操作。这对于实现“无锁”或“低锁”的共享指针更新场景非常有用,例如当你想原子地替换一个
    shared_ptr
    登录后复制
    所指向的整个对象时。

    #include <iostream>
    #include <memory>
    #include <thread>
    #include <atomic>
    #include <vector>
    
    class MyHeavyData {
    public:
        int id;
        MyHeavyData(int i) : id(i) {
            // std::cout << "MyHeavyData " << id << " created." << std::endl;
        }
        ~MyHeavyData() {
            // std::cout << "MyHeavyData " << id << " destroyed." << std::endl;
        }
    };
    
    std::atomic<std::shared_ptr<MyHeavyData>> atomic_data;
    
    void writer_atomic() {
        for (int i = 0; i < 5; ++i) {
            std::shared_ptr<MyHeavyData> new_ptr = std::make_shared<MyHeavyData>(i);
            atomic_data.store(new_ptr); // 原子地更新指针
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    
    void reader_atomic() {
        for (int i = 0; i < 10; ++i) {
            std::shared_ptr<MyHeavyData> current_ptr = atomic_data.load(); // 原子地读取指针
            if (current_ptr) {
                // std::cout << "Reader got data ID: " << current_ptr->id << std::endl;
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(5));
        }
    }
    
    // int main() {
    //     atomic_data.store(std::make_shared<MyHeavyData>(-1)); // 初始值
    //     std::vector<std::thread> threads;
    //     threads.emplace_back(writer_atomic);
    //     threads.emplace_back(reader_atomic);
    //     threads.emplace_back(reader_atomic);
    //     for (auto& t : threads) {
    //         t.join();
    //     }
    //     return 0;
    // }
    登录后复制

    需要强调的是,

    std::atomic<std::shared_ptr<T>>
    登录后复制
    只保证了指针本身的原子操作(load, store, exchange, compare_exchange_weak/strong),它并保护
    T
    登录后复制
    类型对象内部的数据。如果你通过
    current_ptr
    登录后复制
    去修改
    current_ptr->id
    登录后复制
    ,那仍然需要额外的同步。它的主要用途是当你希望原子地“切换”
    shared_ptr
    登录后复制
    所指向的整个对象实例时,例如,一个配置管理器需要加载新的配置对象并替换旧的配置对象。

shared_ptr
登录后复制
在多线程中传递和生命周期管理有哪些需要注意的细节?

在多线程中使用

shared_ptr
登录后复制
,除了数据保护,其传递方式和生命周期管理也是一个充满细节和潜在陷阱的领域。

  1. 传递方式的选择:按值、按

    const
    登录后复制
    引用还是
    weak_ptr
    登录后复制

    • 按值传递 (
      std::shared_ptr<T> p
      登录后复制
      ):
      当你希望函数调用者和被调用者共享对象的所有权,并确保对象在函数执行期间不会被销毁时,应该按值传递。这会增加引用计数,确保对象存活。这是最常见的共享所有权的方式。
    • const
      登录后复制
      引用传递 (
      const std::shared_ptr<T>& p
      登录后复制
      ):
      当函数只需要访问
      shared_ptr
      登录后复制
      指向的对象,但不需要共享或延长其生命周期时,使用
      const
      登录后复制
      引用。这避免了不必要的引用计数增减开销,但要求调用者保证
      shared_ptr
      登录后复制
      在函数执行期间仍然有效。
    • 使用
      std::weak_ptr
      登录后复制
      weak_ptr
      登录后复制
      是一个不拥有对象所有权的智能指针。它不会增加引用计数,因此不会阻止对象被销毁。它主要用于解决
      shared_ptr
      登录后复制
      可能导致的循环引用问题,或者当你只是想“观察”一个对象,而不想影响其生命周期时。在多线程环境中,一个线程可以持有一个
      weak_ptr
      登录后复制
      ,然后尝试通过
      lock()
      登录后复制
      方法将其提升为
      shared_ptr
      登录后复制
      。如果对象仍然存活,
      lock()
      登录后复制
      会返回一个有效的
      shared_ptr
      登录后复制
      ;否则,返回一个空的
      shared_ptr
      登录后复制
      。这个
      lock()
      登录后复制
      操作本身是线程安全的。
    #include <iostream>
    #include <memory>
    #include <thread>
    #include <chrono>
    
    class MyObject {
    public:
        int id;
        MyObject(int i) : id(i) { std::cout << "Object " << id << " created." << std::endl; }
        ~MyObject() { std::cout << "Object " << id << " destroyed." << std::endl; }
    };
    
    void observe_object(std::weak_ptr<MyObject> weak_obj) {
        std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 模拟一些延迟
        if (std::shared_ptr<MyObject> locked_obj = weak_obj.lock()) {
            std::cout << "Observer: Object " << locked_obj->id << " is still alive." << std::endl;
        } else {
            std::cout << "Observer: Object has been destroyed." << std::endl;
        }
    }
    
    // int main() {
    //     std::shared_ptr<MyObject> shared_obj = std::make_shared<MyObject>(10);
    //     std::thread t(observe_object, std::weak_ptr<MyObject>(shared_obj));
    //     shared_obj.reset(); // 主线程释放shared_ptr,对象可能会被销毁
    //     t.join();
    //     return 0;
    // }
    登录后复制
  2. 避免循环引用导致的内存泄漏: 这是

    shared_ptr
    登录后复制
    在复杂对象图中一个经典的陷阱。如果对象A通过
    shared_ptr
    登录后复制
    持有对象B,同时对象B也通过
    shared_ptr
    登录后复制
    持有对象A,那么它们的引用计数永远不会降到零,导致这两个对象及其所有资源都无法被释放,形成内存泄漏。
    weak_ptr
    登录后复制
    正是解决此问题的利器。在这种情况下,通常让其中一个引用(例如B指向A的引用)改为
    weak_ptr
    登录后复制

  3. this
    登录后复制
    指针创建
    shared_ptr
    登录后复制
    enable_shared_from_this
    登录后复制
    在一个类的

以上就是C++如何在多线程中安全使用shared_ptr的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号