Вложенное уничтожение shared_ptr вызывает переполнение стека
Это проблема дизайна (я знаю, почему это происходит, просто хочу посмотреть, как люди справляются с ней). Предположим, у меня есть простой связанный список struct
:
struct List {
int head;
std::shared_ptr<List> tail;
};
shared_ptr
позволяет обмениваться подсписками между несколькими списками. Однако, когда список становится очень длинным, в его деструкторе может произойти переполнение стека (вызванное рекурсивными выпусками shared_ptr
s). Я попытался использовать явный стек, но это очень сложно, поскольку хвост может принадлежать нескольким спискам. Как я могу создать свой List
, чтобы избежать этой проблемы?
ОБНОВЛЕНИЕ: Чтобы уточнить, я не изобретаю колесо (std::forward_list
). List
выше - это только упрощенная версия реальной структуры данных. Реальная структура данных представляет собой ориентированный ациклический граф, который, если вы думаете об этом, - это всего лишь много связанных списков с общими хвостами/головами. Обычно копировать граф обычно не стоит дорого, поэтому необходим обмен данными.
ОБНОВЛЕНИЕ 2: Я думаю о явном пересечении цепочки указателей и std::move
, когда я иду. Что-то вроде:
~List()
{
auto p = std::move(tail);
while (p->tail != nullptr && p->tail.use_count() == 1) {
// Some other thread may start pointing to `p->tail`
// and increases its use count before the next line
p = std::move(p->tail);
}
}
Кажется, что это работает в одном потоке, но я беспокоюсь о безопасности потоков.
Ответы
Ответ 1
Если у вас возникли проблемы с переполнением стека при уничтожении вашей связанной структуры данных, самым простым решением является простое выполнение отложенной очистки:
struct Graph {
std::shared_ptr<Graph> p1, p2, p3; // some pointers in your datastructure
static std::list<std::shared_ptr<Graph>> deferred_cleanup;
~Graph() {
deferred_cleanup.emplace_back(std::move(p1));
deferred_cleanup.emplace_back(std::move(p2));
deferred_cleanup.emplace_back(std::move(p3));
}
static void cleanup() {
while (!deferred_cleanup.empty()) {
std::list<std::shared_ptr<Graph>> tmp;
std::swap(tmp, deferred_cleanup);
tmp.clear(); } }
};
и вам просто нужно периодически называть Graph::cleanup();
.
Ответ 2
это должно это сделать. С небольшой работой он может быть легко выполнен в потоковом режиме (небольшая блокировка/атомизация в двигателе с отработавшим двигателем)
синопсис:
Разделение shared_ptr на узлы создаются с помощью специального деструктора, который вместо удаления node передает его на двигатель делетирования.
Реализация двигателя - одноэлементная. После уведомления о новом node, который нужно удалить, он добавляет node в очередь удаления. Если не удалено node, узлы в очереди удаляются по очереди (без рекурсии).
Пока это происходит, новые узлы, поступающие в движок, просто добавляются к задней части очереди. Цикл удаления в процессе будет достаточно быстро о них позаботиться.
#include <memory>
#include <deque>
#include <stdexcept>
#include <iostream>
struct node;
struct delete_engine
{
void queue_for_delete(std::unique_ptr<node> p);
struct impl;
static impl& get_impl();
};
struct node
{
node(int d) : data(d) {}
~node() {
std::cout << "deleting node " << data << std::endl;
}
static std::shared_ptr<node> create(int d) {
return { new node(d),
[](node* p) {
auto eng = delete_engine();
eng.queue_for_delete(std::unique_ptr<node>(p));
}};
}
int data;
std::shared_ptr<node> child;
};
struct delete_engine::impl
{
bool _deleting { false };
std::deque<std::unique_ptr<node>> _delete_list;
void queue_for_delete(std::unique_ptr<node> p)
{
_delete_list.push_front(std::move(p));
if (!_deleting)
{
_deleting = true;
while(!_delete_list.empty())
{
_delete_list.pop_back();
}
_deleting = false;
}
}
};
auto delete_engine::get_impl() -> impl&
{
static impl _{};
return _;
}
void delete_engine::queue_for_delete(std::unique_ptr<node> p)
{
get_impl().queue_for_delete(std::move(p));
}
struct tree
{
std::shared_ptr<node> root;
auto add_child(int data)
{
if (root) {
throw std::logic_error("already have a root");
}
auto n = node::create(data);
root = n;
return n;
}
};
int main()
{
tree t;
auto pc = t.add_child(6);
pc = pc->child = node::create(7);
}
Ответ 3
std:: shared_ptr (и до этого boost:: shared_ptr) является и является стандартом де-факто для построения динамических систем с участием массивных DAG.
В действительности, группы DAG не получают такой глубокий (возможно, 10 или 12 алгоритмов в глубину вашего среднего сервера оценки FX?), поэтому рекурсивные удаления не являются проблемой.
Если вы думаете о создании огромной DAG с глубиной 10 000, тогда это может стать проблемой, но, честно говоря, я думаю, что это будет наименьшее из ваших беспокойств.
Повторяется аналогия с DAG как связанный список... на самом деле. Поскольку это ациклично, все ваши указатели, указывающие "вверх", должны быть shared_ptr, и все ваши обратные указатели (например, привязка подписных сообщений к алгоритмам раковины) должны быть слабыми_ptr, которые вы блокируете при запуске сообщения.
отказ от ответственности: я потратил много времени на разработку и построение информационных систем на основе ориентированных ациклических графиков параметризованных компонентов алгоритма с большим количеством общих компонентов (т.е. одного и того же алгоритма с одинаковыми параметрами).
Эффективность графика никогда не является проблемой. Узкие места:
-
изначально создавая график при запуске программы - там много шума в этой точке, но это происходит только один раз.
-
получение данных в процессе и из процесса (обычно это шина сообщений). Это неизбежно является узким местом, поскольку оно включает в себя операции ввода-вывода.