首页 > 其他分享 >深入理解 `std::shared_ptr`:原理、用法及其线程安全性

深入理解 `std::shared_ptr`:原理、用法及其线程安全性

时间:2024-11-04 13:57:16浏览次数:1  
标签:std 计数 线程 引用 shared ptr

在 C++ 中,智能指针是现代内存管理的重要工具,尤其是在复杂的多线程环境中,能显著减少内存泄漏和悬空指针等问题。std::shared_ptr 是 C++11 引入的一种共享智能指针,通过引用计数机制管理对象的生命周期。本文将详细介绍 std::shared_ptr 的基本用法、循环引用问题、线程安全性及其局限性。


1. 什么是 std::shared_ptr

std::shared_ptr 是 C++ 标准库中的一种智能指针,允许多个指针共享管理同一个对象的生命周期。它通过引用计数(reference count)来记录有多少个指针指向同一个对象,当引用计数为零时,std::shared_ptr 会自动释放对象,避免手动管理内存带来的风险。

#include <iostream>
#include <memory>

void example() {
    std::shared_ptr<int> p1 = std::make_shared<int>(10); // p1 引用计数为 1
    std::shared_ptr<int> p2 = p1; // p1 和 p2 都指向同一个 int,引用计数为 2

    std::cout << *p1 << std::endl; // 输出 10

    p2.reset(); // p2 被重置,引用计数减少为 1
} // 作用域结束,p1 被销毁,引用计数为 0,对象被释放

在上面的例子中,std::shared_ptr 可以安全地管理内存的分配和释放,保证了在作用域结束时对象被自动释放。


2. std::shared_ptr 的优势

使用 std::shared_ptr 带来了以下主要优势:

  • 自动释放:当最后一个 std::shared_ptr 离开作用域时,引用计数变为零,自动调用对象的析构函数,防止内存泄漏。
  • 对象共享:多个 std::shared_ptr 可以指向同一对象,简化了资源共享的实现。
  • 异常安全std::shared_ptr 的引用计数会自动管理,不会因为函数异常退出而泄漏内存。

这些优势使 std::shared_ptr 特别适合用于对象共享和复杂的生命周期管理。


3. 循环引用问题

尽管 std::shared_ptr 带来了诸多便利,但它的引用计数机制也可能带来循环引用问题。循环引用发生在两个或多个对象相互引用对方的 std::shared_ptr,导致引用计数永远无法归零,进而造成内存泄漏。

示例:循环引用问题

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B destroyed\n"; }
};

void example() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();

    a->b_ptr = b;
    b->a_ptr = a;

    // 离开作用域时,A 和 B 的析构函数不会被调用,造成内存泄漏
}

在上述代码中,AB 互相持有 std::shared_ptr,因此即使 example 结束,ab 的引用计数也不会归零,导致析构函数未被调用。为了解决循环引用问题,C++ 提供了 std::weak_ptr


4. 使用 std::weak_ptr 打破循环引用

std::weak_ptr 是一种弱引用,它不会影响 std::shared_ptr 的引用计数,因此可以避免循环引用问题。std::weak_ptr 的主要作用是打破循环引用,同时提供一种安全的方式来访问 std::shared_ptr 所管理的对象。

示例:使用 std::weak_ptr 解决循环引用

#include <iostream>
#include <memory>

class B;

class A {
public:
    std::weak_ptr<B> b_ptr; // 使用 weak_ptr 避免循环引用
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // 使用 weak_ptr 避免循环引用
    ~B() { std::cout << "B destroyed\n"; }

    void useA() {
        if (auto shared_a = a_ptr.lock()) { // 使用 lock() 获取 shared_ptr
            std::cout << "Using A\n";
        } else {
            std::cout << "A 已被释放,无法使用\n";
        }
    }
};

void example() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();

    a->b_ptr = b;
    b->a_ptr = a;

    b->useA(); // 输出 "Using A"
}

在这个例子中,AB 使用 std::weak_ptr 互相引用,这样就不会增加引用计数,从而避免了循环引用的问题。std::weak_ptrlock() 方法会尝试返回一个有效的 std::shared_ptr,如果对象已经被释放,则返回空的 std::shared_ptr,这样可以安全地检查对象是否有效。


5. std::shared_ptr 的线程安全性

std::shared_ptr 提供了基本的线程安全性,保证了引用计数的线程安全更新。这意味着多个线程可以安全地同时持有和复制同一个 std::shared_ptr,引用计数的递增和递减操作会被正确地同步。

线程安全性带来的好处

  • 引用计数线程安全:在多线程环境中,std::shared_ptr 的引用计数更新是原子操作,无需额外的加锁操作。
  • 自动释放的线程安全性:在最后一个 std::shared_ptr 离开作用域时,std::shared_ptr 会自动释放对象,而这一过程在多线程中是安全的。

示例:多线程使用 std::shared_ptr

#include <iostream>
#include <memory>
#include <thread>

void thread_func(std::shared_ptr<int> ptr) {
    std::cout << "Thread: " << *ptr << std::endl;
}

void example() {
    auto shared_int = std::make_shared<int>(42);

    std::thread t1(thread_func, shared_int);
    std::thread t2(thread_func, shared_int);

    t1.join();
    t2.join();
} // 作用域结束,shared_int 被自动释放

在这个例子中,shared_int 在两个线程之间共享,std::shared_ptr 自动管理引用计数,并确保在多线程环境下引用计数的更新是安全的,避免了计数错误和资源释放问题。

注意事项:虽然 std::shared_ptr 确保了引用计数的线程安全,但对对象本身的访问并非线程安全。如果多个线程要修改 std::shared_ptr 指向的对象,仍然需要额外的同步措施(如使用 std::mutex)来保证线程安全。


6. 多线程修改 std::shared_ptr 指向的对象

如果多个线程需要同时访问并修改 std::shared_ptr 指向的对象,使用 std::mutex 可以保证线程安全。这里提供一个示例展示如何使用 std::mutex 来保护对共享对象的访问和修改。

示例:多线程修改 std::shared_ptr 指向的对象

在这个例子中,我们创建一个共享的计数器对象,多个线程将同时访问并修改该计数器。在没有 std::mutex 保护的情况下,计数器的值可能会因数据竞争而出现错误。通过在访问和修改计数器的代码块中添加互斥锁,我们可以确保每个线程按顺序访问该资源,避免数据竞争。

#include <iostream>
#include <memory>
#include <thread>
#include <mutex>
#include <vector>

class Counter {
public:
    int value;

    Counter() : value(0) {}
    void increment() {
        ++value;
    }
    int getValue() const {
        return value;
    }
};

void thread_func(std::shared_ptr<Counter> counter, std::mutex& mtx) {
    for (int i = 0; i < 100; ++i) {
        std::lock_guard<std::mutex> lock(mtx);  // 加锁保护对 counter 的访问
        counter->increment();
    }
}

int main() {
    auto counter = std::make_shared<Counter>();
    std::mutex mtx;

    std::vector<std::thread> threads;

    // 启动10个线程,每个线程对 counter 执行 100 次 increment 操作
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(thread_func, counter, std::ref(mtx));
    }

    // 等待所有线程完成
    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Final counter value: " << counter->getValue() << std::endl; // 期望输出 1000

    return 0;
}

在这个例子中,Counter 类的对象由 std::shared_ptr 管理,并在多个线程中共享,在 thread_func 函数中,每次调用 counter->increment() 前,都用 std::lock_guard<std::mutex> 锁定 mtx,保证每次访问 increment() 是原子操作,std::lock_guardRAII 风格的锁管理器,它会在代码块结束时自动释放锁。启动 10 个线程,每个线程对共享计数器执行 100 次增量操作。通过 std::mutex,我们保证了计数器的修改是线程安全的。

程序输出:Final counter value: 1000

在没有互斥锁的情况下,counter->increment() 在多个线程中可能会发生竞争,导致最终计数值低于预期的 1000。使用 std::mutex 来保护对共享资源的访问,保证了线程安全,确保最终计数器值为 1000。

标签:std,计数,线程,引用,shared,ptr
From: https://www.cnblogs.com/linxmouse/p/18525086

相关文章

  • Python中的生产者-消费者模型:多进程与多线程的实践
    Python中的生产者-消费者模型:多进程与多线程的实践在现代编程中,生产者-消费者模型是一种常见的设计模式,用于处理任务队列和并发执行。Python提供了多种工具来实现这一模型,包括threading模块和multiprocessing模块。本文将通过一个实际的案例——从网页上批量下载图片——来......
  • 【C++】reference to ‘prev‘ is ambiguous:std 命名空间冲突引发的编译错误
    问题描述C++代码编译错误:usingnamespacestd;usingll=longlong;constintN=1e6+7;llprev[N];原因分析在C++的标准库中,std命名空间包含一个名为std::prev的函数,该函数用于获取容器中的前一个迭代器。在上述代码中,通过usingnamespacestd;语句,所......
  • Jenkins/Java 线程泄露排查(二)
    ......
  • 如何捕获线程池执行产生的异常
    1.如何捕获线程池执行产生的异常就像例子1中所写的那样,executor执行一个Runnable接口,在Runnable的实现lambda表达式中trycatch了异常之后,是没办法throwe往外抛出异常的。在main线程中想catch住线程池执行的异常,也catch不到。如果这样写是不行的,体会一下:packagecom.test.threa......
  • 【Python】全面解析Python中的GIL(全局解释器锁):多线程与多进程的实战与抉择
    解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界Python中的GIL(全局解释器锁)对多线程并发执行的限制性影响是开发者在性能优化时需要特别关注的内容。本文将详细讨论GIL的工作机制及其对多线程性能的影响,深入分析Python多线程在CPU密集型和I/O密集型任......
  • Java 多线程售票示例
     1、售票任务类:packagecom.joyupx.cms.example.globalLock5;importcn.hutool.core.date.DateTime;importlombok.extern.slf4j.Slf4j;importstaticjava.lang.Thread.sleep;/***售票任务*@authorhapday*@since0.0.1*/@Slf4jpublicclassSellTicketT......
  • g++ error unrecognized command-line option ‘-std=c++23’; did you mean ‘-std=c
    编译一个C++的项目代码,报错:g++:error:unrecognizedcommand-lineoption‘-std=c++23’;didyoumean‘-std=c++03’?解决方法:安装g++-11,修改系统中的默认指定,将g++系统路径指向新安装的g++-11路径。强化学习算法library库:(集成库)https://github.com/Denys88/rl_ga......
  • C++ 手撕--共享式智能指针(shared_ptr)的简单实现
    C++手撕--共享式智能指针(shared_ptr)的简单实现共享式智能指针(shared_ptr):#include<iostream>#include<mutex>usingnamespacestd;template<typenameT>classShared_ptr{private:T*ptr;int*ref_count;std::mutex*mtx;voidrelease(){......
  • Java 虚拟线程:高并发编程的新纪元(Java 21)
    Java虚拟线程:高并发编程的新纪元前言在现代软件开发中,高并发编程一直是一个具有挑战性的问题。传统的线程模型虽然强大,但在高并发场景下会引发性能瓶颈和资源耗尽等问题。为了应对这些挑战,Java引入了虚拟线程(VirtualThreads),它为高并发编程带来了全新的解决方案。虚拟......
  • 线程安全的单例模式(Singleton)。
    在Java中,实现线程安全的单例模式(Singleton)通常涉及确保类的实例在多线程环境中只被创建一次。有多种方法可以实现这一点,包括使用synchronized关键字、双重检查锁定(Double-CheckedLocking,DCL)、静态内部类(BillPughSingletonDesign)以及使用java.util.concurrent包中的类。......