shared_ptr存在的循环依赖问题是什么?

15

我读过关于共享指针的相关内容并理解了如何使用。但是我从未理解过共享指针中的循环依赖问题,以及弱指针如何解决这些问题。请问有人能够清楚地解释这个问题吗?


2
如果你需要另一个关键词进行搜索,它也被称为“保留循环”。 - Bryan Chen
1
这个回答解决了你的问题吗?一天,一个学生来找Moon并说: - n. m.
4个回答

26

问题并不复杂。让-->表示共享指针:

The rest of the program  --> object A --> object B
                                    ^     |
                                     \    |
                                      \   v
                                        object C

所以我们在共享指针中遇到了循环依赖。每个对象的引用计数是多少?

A:  2
B:  1
C:  1

现在假设程序的其余部分(或者至少持有指向A的共享指针的部分)已经被销毁。那么A的引用计数会减1,因此循环中每个对象的引用计数都为1。那么什么被删除了?什么都没有被删除。但是我们想要被删除的是什么?一切,因为我们的任何对象都无法从程序的其余部分访问。

因此,在这种情况下的解决方法是将从C到A的链接更改为弱指针。弱指针不会影响其目标的引用计数,这意味着当程序的其余部分释放A时,它的引用计数降至0。所以它被删除了,因此B也被删除,C也被删除。

然而,在程序的其余部分释放A之前,C可以随时通过锁定弱指针来访问A。这将使其晋升为共享指针(并将A的引用计数增加到2),只要C正在积极地处理A。这意味着,如果在此期间以其他方式释放A,则其引用计数仅降至1。使用A的C中的代码不会崩溃,并且在锁定弱指针的代码块结束时销毁了短期共享指针,A也被删除了。

一般来说,决定在哪里放置弱指针可能是复杂的。为了选择中断循环的位置,需要在循环中的对象之间具有某种不对称性。在这种情况下,我们知道A是程序其余部分引用的对象,因此我们知道中断循环的位置是指向A的任何对象。


谢谢 Steve。我现在明白了。感谢清晰的解释。 - kadina
1
不确定这是否是解释循环依赖的正确示例。引用计数在控制块内维护,所有共享指针A、B、C将指向同一个控制块,计数为3。 - Manish Baphna
1
@ManishBaphna 不,这里有三个控制块,分别针对对象A、B和C。在这个例子中,最初有四个(未标记的)共享指针。 - Caleth

10
shard_ptr<A> <----| shared_ptr<B> <------
    ^             |          ^          |
    |             |          |          |
    |             |          |          |
    |             |          |          |
    |             |          |          |
class A           |     class B         |
    |             |          |          |
    |             ------------          |
    |                                   |
    -------------------------------------

现在如果我们将类B和A的shared_ptr创建出来,那么两个指针的use_count都为2。

当shared_ptr超出作用域时,计数仍然保持为1,因此A和B对象不会被删除。

class B;

class A
{
    shared_ptr<B> sP1; // use weak_ptr instead to avoid CD

public:
    A() {  cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }

    void setShared(shared_ptr<B>& p)
    {
        sP1 = p;
    }
};

class B
{
    shared_ptr<A> sP1;

public:
    B() {  cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }

    void setShared(shared_ptr<A>& p)
    {
        sP1 = p;
    }
};

int main()
{
    shared_ptr<A> aPtr(new A);
    shared_ptr<B> bPtr(new B);

    aPtr->setShared(bPtr);
    bPtr->setShared(aPtr);

    return 0;  
}

输出:

A()
B()

从输出结果可以看出,A和B指针从未被删除,因此存在内存泄漏问题。

为了避免这种情况,只需在A类中使用weak_ptr而非shared_ptr更为合理。


1
我认为这样的解释还不够。如果类B的对象被销毁会发生什么?在“for dummy”的解释中,它会在自身内部销毁对象。因此,它会调用shared_ptr对象的销毁,从而导致类A的对象被销毁。好的,A销毁了指向B对象的shared_ptrA将看到B处于哪种状态:已销毁/半销毁/仍然存在?接下来呢?为什么在运行时忽略这种行为?为什么不抛出异常? - DisplayName

0

如果您了解循环依赖关系,那么可以继续使用shared_ptr而不切换到weak_ptr,但对象的删除需要一些手动工作。以下代码修改自Swapnil的答案。

#include <iostream>
#include <memory>

using namespace std ;

class B;

class A
{
   shared_ptr<B> sP1; // use weak_ptr instead to avoid CD

public:
   A() {  cout << "A()" << endl; }
   ~A() { cout << "~A()" << endl; }

   void setShared(shared_ptr<B>& p)
   {
       sP1 = p;
   }

   // nullifySharedPtr cuts the circle of reference
   // once this is triggered, then the ice can be broken
   void nullifySharedPtr() {
      sP1 = nullptr; 
   }

};

class B
{
   shared_ptr<A> sP1;

public:
   B() {  cout << "B()" << endl; }
   ~B() { cout << "~B()" << endl; }

   void setShared(shared_ptr<A>& p)
   {
       sP1 = p;
   }
};

int main()
{
   shared_ptr<A> aPtr(new A);
   shared_ptr<B> bPtr(new B);
   
   aPtr->setShared(bPtr);
   bPtr->setShared(aPtr);

   cout << aPtr.use_count() << endl;
   cout << bPtr.use_count() << endl;

   // to be break the ice:
   aPtr->nullifySharedPtr() ;
   
   return 0;  
}

nullifySharedPtr 作为一把剪刀,可以剪断圆圈,从而使系统能够完成自己的删除工作。


0

问题本身如上所示。解决方案如下:

  • 按照roy.atlas的手动断开。
  • 通过设计进行断言,您拥有一个树形结构,没有循环,例如在XML-DOM树中,您将父关系作为弱指针放置。
  • 在通用解决方案中,只要存在一条从局部或静态变量(即不在堆上的任何对象)到共享指针链的链路,对象就会存在。为此,您需要能够检测对象的内存段类型,这取决于运行时环境;并且类的每个shared_ptr成员都必须知道周围的实例。现在,当删除shared_ptr时,引用的对象可以从非堆对象找到替代路径,否则将被销毁。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接