shared_ptr的安全性

1. shared_ptr是否安全?

        `std::shared_ptr` 是 C++ 标准库提供的一种智能指针,它可以确保动态分配的对象在适当的时间被安全地删除。`shared_ptr` 使用引用计数机制来跟踪有多少个 `shared_ptr` 实例指向同一个对象。当最后一个这样的指针被销毁时(或者被赋予新的对象),它指向的对象也会被删除。

关于 `std::shared_ptr` 的安全性,有几个层面需要考虑:

1.1 内存管理安全性:
          `shared_ptr` 提供了自动的内存管理,这意味着它可以帮助避免内存泄漏。当最后一个 `shared_ptr` 指向的对象不再被引用时,对象会被自动销毁,内存会被释放。

1.2. 线程安全性:
         对 `shared_ptr` 本身的操作是线程安全的:`std::shared_ptr` 在修改引用计数时使用原子操作,这意味着在多线程环境中,你可以安全地复制和销毁指向同一对象的 `shared_ptr` 实例。
        对所指对象的操作不一定是线程安全的:即使 `shared_ptr` 能够保证其管理的对象在多个线程间共享时不会出现内存管理上的问题,它并不保证多个线程同时访问所指向的对象的成员或函数是安全的。对象的线程安全性需要通过其他机制(如互斥锁)来保证。       

假设你有一个 `std::shared_ptr` 指向一个类 `Foo` 的实例,
这个类有一个方法 `bar()` 和一个成员变量 `baz`。

class Foo {
public:
    int baz;

    void bar() {
        // 这里进行一些操作,比如:
        baz += 1;
    }
};

        现在,你在两个不同的线程中使用同一个 `std::shared_ptr<Foo>` 实例来调用 `bar()` 方法:

void thread_function(std::shared_ptr<Foo> foo_ptr) {
    for (int i = 0; i < 1000000; ++i) {
        foo_ptr->bar();
    }
}

int main() {
    std::shared_ptr<Foo> foo = std::make_shared<Foo>();

    std::thread t1(thread_function, foo);
    std::thread t2(thread_function, foo);

    t1.join();
    t2.join();

    std::cout << foo->baz << std::endl;
    return 0;
}

        在上面的例子中,两个线程 `t1` 和 `t2` 都在调用相同对象的 `bar()` 方法。因为 `bar()` 方法修改了 `Foo` 对象中的 `baz` 成员变量,这个操作是有数据竞争的。在没有适当的同步机制(如互斥锁)的情况下,两个线程可能会同时读取、修改和写入 `baz`,导致最终结果不确定和不正确。

        这里是一个修正后的线程安全的版本:


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

class Foo {
public:
    int baz;
    std::mutex mtx;

    void bar() {
        std::lock_guard<std::mutex> lock(mtx); // 使用互斥锁保证线程安全
        baz += 1;
    }
};

void thread_function(std::shared_ptr<Foo> foo_ptr) {
    for (int i = 0; i < 1000000; ++i) {
        foo_ptr->bar();
    }
}

int main() {
    std::shared_ptr<Foo> foo = std::make_shared<Foo>();

    std::thread t1(thread_function, foo);
    std::thread t2(thread_function, foo);

    t1.join();
    t2.join();
    /*
    在 C++ 中,`std::thread::join` 函数的作用是阻塞当前线程直到由 `std::thread` 对象表示的线        
    程执行完毕。换句话说,如果在一个线程 A 中调用了另一个线程 B 的 `join` 方法,那么线程 A 会等 
    待,直到线程 B 结束它的执行。
    这两行代码确保了主线程(执行 `main` 函数的线程)会等待 `t1` 和 `t2` 这两个子线程完成它们的 
    `thread_function` 函数的执行。如果没有这两个 `join` 调用,`main` 函数可能会在 `t1` 和 
    `t2` 完成之前结束,从而导致程序结束,这可能会造成 `t1` 和 `t2` 中的代码没有被完全执行。
    */

    std::cout << foo->baz << std::endl;
    return 0;
}

        在这个修正后的版本中,`bar()` 方法中的操作现在是由一个 `std::mutex` 保护的,这确保了即使在多线程环境下,每次只有一个线程能够修改 `baz`。这样,即使 `shared_ptr` 被多个线程共享,对 `Foo` 对象的访问也是安全的。

        重要的一点是,`std::shared_ptr` 本身保证了它的引用计数的线程安全性,但是它并不保证所指对象的成员或方法的线程安全性。

1.3. 资源泄漏风险:
        循环引用问题:
如果两个或多个 `shared_ptr` 相互持有对方的引用,可能会创建循环引用,导致引用计数永远不会降到零,从而引起内存泄漏。这种情况下应该使用 `std::weak_ptr` 来打破循环。

        在C++中,`std::weak_ptr` 是一种智能指针,它设计用来解决`std::shared_ptr`智能指针可能导致的循环引用问题。循环引用发生在两个或多个`shared_ptr`相互持有对方的引用时,这会阻止引用计数降到零,导致内存泄漏。

        `weak_ptr`不会增加对象的引用计数,它只是提供了一种监视`shared_ptr`管理的对象的方式,而不拥有该对象。因此,一个`weak_ptr`可以从一个`shared_ptr`或另一个`weak_ptr`被构造,但它不会影响其引用计数。

下面是几个关键点来理解`weak_ptr`:

非拥有性质:`weak_ptr`不对其指向的对象进行所有权管理。这意味着`weak_ptr`的存在不会阻止所指向的对象被销毁。
从`shared_ptr`创建:`weak_ptr`通常是从一个`shared_ptr`创建的,用于监视`shared_ptr`的指针而不创建强引用。
临时提升:你可以试图通过调用`lock()`方法来将`weak_ptr`提升为`shared_ptr`,如果所监视的对象还活着(即引用计数大于0),`lock()`会成功返回一个有效的`shared_ptr`,否则返回一个空的`shared_ptr`。
用于解决循环引用:如果你有两个对象互相持有对方的`shared_ptr`,它们将永远不会被销毁。使用`weak_ptr`可以避免这种情况,因为`weak_ptr`不增加引用计数。
检查存活:通过`expired()`方法可以检查所追踪的对象是否仍然存在(即是否没有被销毁)。

#include <iostream>
#include <memory>

class B; // 前向声明

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

class B {
public:
    std::weak_ptr<A> a_ptr; // 使用 weak_ptr 来观察 A 的实例
    ~B() { std::cout << "B destroyed" << std::endl; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    
    a->b_ptr = b;
    b->a_ptr = a;
    
    // 现在 a 和 b 互相引用,但由于 B 中的 a_ptr 是 weak_ptr,
    // 它不会增加 A 的引用计数。
    
    std::cout << "End of main" << std::endl; // 主函数结束时输出
    // main 结束时,a 和 b 的 shared_ptr 被销毁。
    // 由于 B 中的 a_ptr 是 weak_ptr,A 的引用计数归零,它会被销毁。
    // 随后,因为 A 是 b_ptr 的唯一持有者,B 也会被销毁。
    return 0;
}

        在这个代码示例中,A 类有一个指向 B 的 std::shared_ptr,而 B 类有一个指向 A 的 std::weak_ptr。由于 std::weak_ptr 不增加引用计数,它允许 A 实例在没有任何 std::shared_ptr 强引用时被销毁。

        当 main 函数结束时,局部变量 a 和 b(都是 std::shared_ptr)的作用域结束,它们会被销毁。在销毁这些智能指针时,它们所指向的对象的引用计数会减少。对于 a,它是 A 实例的唯一拥有者,当它被销毁时,A 的引用计数变为 0,然后 A 的实例会被销毁(这里输出 “A destroyed”)。由于 A 被销毁,B 实例中的 std::weak_ptr 不再指向一个有效对象,但这并不影响 B 自己的生命周期。之后,B 实例也会因为引用计数为 0 而被销毁(输出 “B destroyed”)。

2. 引用计数是怎么计算的?

        在C++中,`std::shared_ptr` 使用引用计数来管理对象的生命周期。引用计数是一个简单的计数器,用来跟踪有多少个 `std::shared_ptr` 实例共同指向同一个对象。每当你用同一个原始指针创建一个新的 `std::shared_ptr` 或者当你将一个 `std::shared_ptr` 赋值给另一个 `std::shared_ptr` 时,引用计数器会增加。当一个 `std::shared_ptr` 被销毁(比如它的作用域结束了)或者不再指向那个对象时,引用计数器会减少。

引用计数的工作流程通常如下:

1. 初始化: 当你创建一个新的 `std::shared_ptr` 指向一个对象时,引用计数被初始化为1。

std::shared_ptr<int> sp1(new int(10)); // 引用计数现在是1

2. 复制:当一个 `std::shared_ptr` 被复制时,引用计数增加。

 std::shared_ptr<int> sp2 = sp1; // 引用计数增加到2

3. 赋值: 当一个 `std::shared_ptr` 通过赋值操作指向另一个 `std::shared_ptr` 指向的对象时,左侧对象的原引用计数减少(如果它已经指向某个对象),右侧对象的引用计数增加。

 std::shared_ptr<int> sp3;
 sp3 = sp1; // sp1的引用计数增加到3

4. 销毁: 当 `std::shared_ptr` 的实例被销毁(例如,它离开了作用域)时,引用计数减少。

 {
    std::shared_ptr<int> sp4 = sp1; // 引用计数增加到4
 } // sp4 被销毁,引用计数减少到3

5. 引用计数为零: 当引用计数减少到0时,没有任何 `std::shared_ptr` 实例指向对象,对象将被删除。

sp1.reset(); // 引用计数减少到2
sp2.reset(); // 引用计数减少到1
sp3.reset(); // 引用计数减少到0,对象被删除

        重要的是要注意,每个 `std::shared_ptr` 实例都必须精确地增加或减少引用计数。为了保持线程安全,修改引用计数的操作通常是原子的,这意味着即使在多线程环境中,引用计数也能正确地增加或减少,而不会发生数据竞争。

        `std::weak_ptr` 不会增加对象的引用计数。它只是提供一个观察 `std::shared_ptr` 所指向对象的方式,而不影响对象的生命周期。如果所有的 `std::shared_ptr` 都被销毁了,不管是否还有 `std::weak_ptr` 存在,对象都会被删除。

        在上面的   `std::weak_ptr`代码中,引用计数如下:

1. 创建 `std::shared_ptr<A>` 指向 `A` 的一个新实例,此时 `A` 的引用计数为1。
2. 创建 `std::shared_ptr<B>` 指向 `B` 的一个新实例,此时 `B` 的引用计数为1。
3. 将 `b` 赋值给 `a->b_ptr`,此时 `B` 实例的引用计数增至2,因为 `a` 和 `a->b_ptr` 都指向同一个 `B` 实例。
4. 将 `a` 赋值给 `b->a_ptr`,但由于 `b->a_ptr` 是 `std::weak_ptr`,它并不增加 `A` 实例的引用计数,所以 `A` 的引用计数仍然是1。

现在,我们有以下的引用计数:

- `A` 实例的引用计数是1(只有 `a` 指向它)。
- `B` 实例的引用计数是2(`b` 和 `a->b_ptr` 指向它)。

当 `main` 函数结束时,局部变量 `a` 和 `b` 会被销毁。这会导致:

- `a` 被销毁,`A` 实例的引用计数减至0,因此 `A` 实例会被销毁,并输出 "A destroyed"。
- `b` 被销毁,`B` 实例的引用计数减至1(因为 `a->b_ptr` 已经不存在了)。然后,由于没有其他 `std::shared_ptr` 指向 `B` 实例,`B` 的引用计数进一步减至0,`B` 实例会被销毁,并输出 "B destroyed"。

        总结来说,程序的输出应该是:

End of main
A destroyed
B destroyed

        程序先输出 "End of main",然后因为 `a` 和 `b` 的作用域结束,它们指向的对象会依次被销毁,先销毁 `A` 实例,然后销毁 `B` 实例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值