В чем проблема циклической зависимости с shared_ptr?

Я прочитал об общих указателях и понял, как использовать. Но я никогда не понимал проблему циклической зависимости с общими указателями и то, как слабые указатели будут решать эти проблемы. Кто-нибудь может объяснить, пожалуйста, эту проблему ясно?

4 ответа

Решение

Проблема не так сложна. Позволять --> представлять общий указатель:

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 освобождается иным образом во время этого процесса, то его refcount падает только до 1. код в C, который использует A, не падает, и A удаляется всякий раз, когда этот кратковременный общий указатель уничтожается. Который находится в конце блока кода, который заблокировал слабый указатель.

В общем, решение о том, где разместить слабые указатели, может быть сложным. Вам нужна какая-то асимметрия среди объектов в цикле, чтобы выбрать место, чтобы разбить его. В этом случае мы знаем, что A - это объект, на который ссылается остальная часть программы, поэтому мы знаем, что место для разрыва цикла - это то, что указывает на A.

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

Теперь, если мы сделаем shared_ptr класса B и A, use_count обоих указателей будет равен двум.

Когда 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 никогда не удаляются и, следовательно, происходит утечка памяти.

Чтобы избежать такой проблемы, просто используйте weak_ptr в классе A вместо shared_ptr, что имеет больше смысла.

Если вы знаете о циклической зависимости, вы можете придерживаться 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действует как ножницы, чтобы вырезать круг и, следовательно, позволяет системе выполнять свою собственную работу по удалению.

Сама проблема отображается выше. Решения

  • Ручная ломка по roy.atlas
  • Утверждая, что у вас есть древовидная структура без циклов, например, в дереве XML-DOM, вы поместите родительское отношение в качестве слабого указателя.
  • В универсальном решении объект будет существовать до тех пор, пока есть цепочка shared_ptr-ов от локальной или статической переменной, т.е. от любого объекта, который не находится в куче. Для этого вам нужно уметь определять тип сегмента памяти объекта, который зависит от среды выполнения; также каждый член класса shared_ptr должен знать окружающий его экземпляр. Теперь при удалении shared_ptr объект, на который ссылаются, может найти альтернативный путь от объекта, не относящегося к куче, иначе он будет уничтожен.
Другие вопросы по тегам