0

0

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

P粉602998670

P粉602998670

发布时间:2025-09-10 08:17:01

|

473人浏览过

|

来源于php中文网

原创

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++在性能和抽象之间寻求平衡的典型体现。

BJXSHOP网上开店专家
BJXSHOP网上开店专家

BJXShop网上购物系统是一个高效、稳定、安全的电子商店销售平台,经过近三年市场的考验,在中国网购系统中属领先水平;完善的订单管理、销售统计系统;网站模版可DIY、亦可导入导出;会员、商品种类和价格均实现无限等级;管理员权限可细分;整合了多种在线支付接口;强有力搜索引擎支持... 程序更新:此版本是伴江行官方商业版程序,已经终止销售,现于免费给大家使用。比其以前的免费版功能增加了:1,整合了论坛

下载

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

正确保护

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

  1. 使用互斥锁(

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

    #include 
    #include 
    #include 
    #include 
    #include 
    
    class MyData {
    public:
        int value;
        MyData(int v = 0) : value(v) {}
        void increment() { value++; }
    };
    
    std::shared_ptr global_data = std::make_shared(0);
    std::mutex data_mutex;
    
    void worker_function() {
        for (int i = 0; i < 10000; ++i) {
            std::lock_guard lock(data_mutex); // 保护数据访问
            global_data->increment();
        }
    }
    
    // int main() {
    //     std::vector 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 
    #include 
    #include 
    #include 
    #include 
    
    class ImmutableData {
    public:
        const int value;
        ImmutableData(int v) : value(v) {}
        // 没有修改成员的方法
    };
    
    std::shared_ptr current_data = std::make_shared(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 lock(update_mutex);
            current_data = std::make_shared(current_data->value + 1); // 创建新对象并更新指针
        }
    }
    
    void reader_function() {
        for (int i = 0; i < 1000; ++i) {
            std::shared_ptr local_copy;
            {
                std::lock_guard 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 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>
    原子化指针更新: C++20引入了
    std::atomic>
    ,它使得
    shared_ptr
    本身的复制和赋值操作成为原子操作。这对于实现“无锁”或“低锁”的共享指针更新场景非常有用,例如当你想原子地替换一个
    shared_ptr
    所指向的整个对象时。

    #include 
    #include 
    #include 
    #include 
    #include 
    
    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> atomic_data;
    
    void writer_atomic() {
        for (int i = 0; i < 5; ++i) {
            std::shared_ptr new_ptr = std::make_shared(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 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(-1)); // 初始值
    //     std::vector 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>
    只保证了指针本身的原子操作(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 p
      ):
      当你希望函数调用者和被调用者共享对象的所有权,并确保对象在函数执行期间不会被销毁时,应该按值传递。这会增加引用计数,确保对象存活。这是最常见的共享所有权的方式。
    • const
      引用传递 (
      const std::shared_ptr& 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 
    #include 
    #include 
    #include 
    
    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 weak_obj) {
        std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 模拟一些延迟
        if (std::shared_ptr 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 shared_obj = std::make_shared(10);
    //     std::thread t(observe_object, std::weak_ptr(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
    在一个类的

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

527

2023.09.20

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

401

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

543

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

53

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

197

2025.08.29

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

482

2023.08.10

Python 多线程与异步编程实战
Python 多线程与异步编程实战

本专题系统讲解 Python 多线程与异步编程的核心概念与实战技巧,包括 threading 模块基础、线程同步机制、GIL 原理、asyncio 异步任务管理、协程与事件循环、任务调度与异常处理。通过实战示例,帮助学习者掌握 如何构建高性能、多任务并发的 Python 应用。

144

2025.12.24

java多线程相关教程合集
java多线程相关教程合集

本专题整合了java多线程相关教程,阅读专题下面的文章了解更多详细内容。

5

2026.01.21

c++ 根号
c++ 根号

本专题整合了c++根号相关教程,阅读专题下面的文章了解更多详细内容。

70

2026.01.23

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go 教程
Go 教程

共32课时 | 4.2万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

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