Как обнаружить циклы при использовании shared_ptr - PullRequest
19 голосов
/ 22 апреля 2009

shared_ptr - интеллектуальный указатель подсчета ссылок в библиотеке Boost.

Проблема с подсчетом ссылок заключается в том, что он не может избавиться от циклов. Мне интересно, как можно было бы решить эту проблему в C ++.

Пожалуйста, не предлагайте никаких предложений, таких как: "не делать циклы" или "использовать слабый_птр".

Редактировать

Мне не нравятся предложения, в которых говорится, что нужно просто использовать слабый_птр, потому что, очевидно, если вы знаете, что создадите цикл, у вас не будет проблем. Вы также не можете знать, что у вас будет цикл во время компиляции, если вы генерируете shared_ptrs во время выполнения.

Так что, пожалуйста, удалите ответы, которые используют в них слабый_птр, потому что я специально просил не иметь таких ответов ...

Ответы [ 10 ]

25 голосов
/ 22 апреля 2009

shared_ptr представляет право собственности отношение. В то время как weak_ptr представляет осведомленность . Наличие нескольких объектов, владеющих друг другом, означает, что у вас есть проблемы с архитектурой, которая решается путем замены одного или нескольких собственных на с (то есть weak_ptr ).

Я не понимаю, почему предложение weak_ptr считается бесполезным.

17 голосов
/ 07 августа 2009

Я понимаю ваше раздражение от того, что мне ловко сказали, чтобы использовать weak_ptr для разрыва циклических ссылок, и я почти чувствую ярость, когда мне говорят, что циклические ссылки - плохой стиль программирования.

Ваш вопрос, в частности, как вы обнаруживаете циклические ссылки. Правда в том, что в сложном проекте некоторые эталонные циклы являются косвенными и их трудно обнаружить.

Ответ заключается в том, что вы не должны делать ложных заявлений, которые делают вас уязвимыми для циклических ссылок. Я серьезно и критикую очень популярную практику - вслепую использую shared_ptr для всего.

В вашем дизайне должно быть ясно, какие указатели являются владельцами, а какие - наблюдателями.

Для владельцев использовать shared_ptr

Для наблюдателей используйте слабый_птр - все они, а не только те, которые, по вашему мнению, могут быть частью цикла.

Если вы будете следовать этой практике, циклические ссылки не вызовут никаких проблем, и вам не нужно о них беспокоиться. Конечно, у вас будет много кода для преобразования всех этих слабых_приемников в shared_ptrs, когда вы захотите их использовать - Boost действительно не подходит для работы.

3 голосов
/ 22 апреля 2009

Довольно легко обнаружить циклы:

  • установить счетчик на какое-то большое число, скажем, 1000 (точный размер зависит от вашего приложения)
  • начните с интересующего вас пионера и следуйте указателям из него
  • для каждого указателя, за которым вы следуете, уменьшите счетчик
  • если счетчик падает до нуля до того, как вы достигнете конца цепочки указателей, у вас будет цикл

Это, однако, не очень полезно. И, как правило, невозможно решить проблему цикличности для пересчитанных указателей - поэтому были изобретены альтернативные схемы сбора мусора, такие как удаление поколений.

2 голосов
/ 22 апреля 2009

Может быть комбинация boost::weak_ptr и boost::shared_ptr? Эта статья может представлять интерес.

2 голосов
/ 22 апреля 2009

Я не нашел гораздо лучшего метода, чем рисование больших UML-графиков и поиск циклов.

Для отладки я использую счетчик экземпляров, идущий в реестр, например:

template <DWORD id>
class CDbgInstCount
{
public:
#ifdef _DEBUG
   CDbgInstCount()   { reghelper.Add(id, 1); }
   CDbgInstCount(CDbgInstCount const &) {  reghelper.Add(id, 1); }
   ~CDbgInstCount()  { reghelper.Add(id, -1); }
#else
#endif
};

Мне просто нужно добавить это к рассматриваемым классам и взглянуть на реестр.

(Идентификатор, если он указан, например, как 'XYZ!', Будет преобразован в строку. К сожалению, вы не можете указать строковую константу в качестве параметра шаблона)

1 голос
/ 26 мая 2017

Возможно, вам нужна техника сбора мусора, такая как Mark and Sweep . Идея этого алгоритма:

  1. Хранить список со ссылками на все выделенные блоки памяти.
  2. В какой-то момент вы запускаете сборщик мусора:
    1. Сначала отмечаются все блоки, к которым он все еще имеет доступ, без использования списка ссылок.
    2. Он просматривает список, стирая каждый элемент, который не может быть помечен, подразумевая, что он больше недоступен, поэтому он бесполезен.

Поскольку вы используете shared_ptr, любые все еще существующие указатели, которых вы не можете найти, должны рассматриваться как члены цикла.

Осуществление

Ниже я опишу очень наивный пример реализации части алгоритма sweep(), но он будет reset() всеми остальными указателями на коллекторе.

В этом коде хранятся shared_ptr<Cycle_t> указатели. Класс Collector отвечает за отслеживание всех указателей и удаление их при выполнении sweep().

#include <vector>
#include <memory>

class Cycle_t;
typedef std::shared_ptr<Cycle_t> Ref_t;

// struct Cycle;
struct Cycle_t {
  Ref_t cycle;

  Cycle_t() {}
  Cycle_t(Ref_t cycle) : cycle(cycle) {}
};

struct collector {
  // Note this vector will grow endlessy.
  // You should find a way to reuse old links
  std::vector<std::weak_ptr<Cycle_t>> memory;

  // Allocate a shared pointer keeping
  // a weak ref on the memory vector:
  inline Ref_t add(Ref_t ref) {
    memory.emplace_back(ref);
    return ref;
  }
  inline Ref_t add(Cycle_t value) {
    Ref_t ref = std::make_shared<Cycle_t>(value);
    return add(ref);
  }
  inline Ref_t add() {
    Ref_t ref = std::make_shared<Cycle_t>();
    return add(ref);
  }

  void sweep() {
    // Run a sweep algorithm:
    for (auto& ref : memory) {
      // If the original shared_ptr still exists:
      if (auto ptr = ref.lock()) {
        // Reset each pointer contained within it:
        ptr->cycle.reset();

        // Doing this will trigger a deallocation cascade, since
        // the pointer it used to reference will now lose its
        // last reference and be deleted by the reference counting
        // system.
        //
        // The `ptr` pointer will not be deletd on the cascade
        // because we still have at least the current reference
        // to it.
      }
      // When we leave the loop `ptr` loses its last reference
      // and should be deleted.
    }
  }
};

Затем вы можете использовать его так:

Collector collector;

int main() {
  // Build your shared pointers:
  {
    // Allocate them using the collector:
    Ref_t c1 = collector.add();
    Ref_t c2 = collector.add(c1);

    // Then create the cycle:
    c1.get()->cycle = c2;

    // A normal block with no cycles:
    Ref_t c3 = collector.add();
  }

  // In another scope:
  {
    // Note: if you run sweep an you still have an existing
    // reference to one of the pointers in the collector
    // you will lose it since it will be reset().
    collector.sweep();
  }
}

Я проверил его с помощью Valgrind, и в списке не было утечек памяти или «все еще доступных» блоков, поэтому он, вероятно, работает как ожидалось.

Некоторые замечания по этой реализации:

  1. Вектор памяти будет бесконечно увеличиваться, вы должны использовать некоторую технику выделения памяти , чтобы убедиться, что он не занимает всю вашу рабочую память.
  2. Можно утверждать, что нет необходимости использовать shared_ptr (который работает как GC подсчета ссылок) для реализации такого сборщика мусора, поскольку алгоритм Mark and Sweep уже позаботится о работе.
  3. Я не реализовал функцию mark (), потому что это усложнит пример, но это возможно, и я объясню это ниже.

Наконец, если вас интересует (2), такого рода реализация не является неслыханной. CPython (основная реализация Python) действительно использует смесь подсчета ссылок, Mark и Sweep, но в основном по историческим причинам .

Реализация функции mark():

Для реализации функции mark() вам потребуется внести некоторые изменения:

Требуется добавить атрибут bool marked; к Cycle_t и использовать его для проверки, отмечен ли указатель или нет.

Вам нужно написать функцию Collector::mark(), которая будет выглядеть следующим образом:

void mark(Ref_t root) {
  root->marked = true;

  // For each other Ref_t stored on root:
  for (Ref_t& item : root) {
    mark(item);
  }
}

А затем вы должны изменить функцию sweep(), чтобы удалить метку, если указатель помечен, или reset() указатель:

void sweep() {
  // Run a sweep algorithm:
  for (auto& ref : memory) {
    // If it still exists:
    if (auto ptr = ref.lock()) {
      // And is marked:
      if (ptr->marked) {
        ptr->marked = false;
      } else {
        ptr->cycle.reset();
      }
    }
  }
}

Это было длинное объяснение, но я надеюсь, что оно кому-нибудь поможет.

1 голос
/ 23 апреля 2009

Общее решение для нахождения цикла можно найти здесь:

Лучший алгоритм для проверки наличия в связанном списке цикла

Это предполагает, что вы знаете структуру объектов в списке и можете следовать всем указателям, содержащимся в каждом объекте.

1 голос
/ 22 апреля 2009

См. Этот пост на обнаружение циклов на графике.

0 голосов
/ 04 мая 2013

Ответьте на старый вопрос, вы можете попробовать навязчивый указатель, который может подсчитать, сколько раз ссылается ресурс.

#include <cstdlib>
#include <iostream>

#include <boost/intrusive_ptr.hpp>

class some_resource
{
    size_t m_counter;

public:
    some_resource(void) :
        m_counter(0)
    {
        std::cout << "Resource created" << std::endl;
    }

    ~some_resource(void)
    {
        std::cout << "Resource destroyed" << std::endl;
    }

    size_t refcnt(void)
    {
        return m_counter;
    }

    void ref(void)
    {
        m_counter++;
    }

    void unref(void)
    {
        m_counter--;
    }
};

void
intrusive_ptr_add_ref(some_resource* r)
{
    r->ref();
    std::cout << "Resource referenced: " << r->refcnt()
              << std::endl;
}

void
intrusive_ptr_release(some_resource* r)
{
    r->unref();
    std::cout << "Resource unreferenced: " << r->refcnt()
              << std::endl;
    if (r->refcnt() == 0)
        delete r;
}

int main(void)
{
    boost::intrusive_ptr<some_resource> r(new some_resource);
    boost::intrusive_ptr<some_resource> r2(r);

    std::cout << "Program exiting" << std::endl;

    return EXIT_SUCCESS;
}

Вот возвращаемый результат.

Resource created 
Resource referenced: 1 
Resource referenced: 2 
Program exiting 
Resource unreferenced: 1
Resource unreferenced: 0 
Resource destroyed
*** Program Exit ***
0 голосов
/ 22 апреля 2009

Я знаю, что вы сказали "нет слабого_птр", но почему бы и нет? Если у вас голова с ослабленным хвостом к хвосту, а голова со слабым хвостом к голове предотвратит цикл.

...