Раннее завершение и утечки памяти в библиотеке C++/CLI
У меня проблемы с финализаторами, которые, кажется, были вызваны в начале проекта C++/CLI (и C#), над которым я работаю. Кажется, это очень сложная проблема, и я собираюсь упомянуть множество различных классов и типов из кода. К счастью, это открытый исходный код, и вы можете следить за ним здесь: Pstsdk.Net ( Mercurial Reository). Я также пытался связываться напрямую с файловым браузером, где это уместно, чтобы вы могли просматривать код во время чтения. Большая часть кода, с которым мы имеем дело, находится в pstsdk.mcpp
папка репозитория.
Код сейчас находится в довольно отвратительном состоянии (я работаю над этим), и текущая версия кода, над которым я работаю, находится в Finalization fixes (UNSTABLE!)
ветка. В этой ветке есть две ревизии, и чтобы понять мой скучный вопрос, нам нужно разобраться с обоими. (изменения: ee6a002df36f и a12e9f5ea9fe)
Для некоторого фона этот проект является оболочкой C++ / CLI неуправляемой библиотеки, написанной на C++. Я не являюсь координатором проекта, и есть несколько проектных решений, с которыми я не согласен, так как я уверен, что многие из вас, кто смотрит на код, будут, но я отвлекся. Мы обертываем большую часть слоев исходной библиотеки в dll C++/CLI, но раскрываем простой в использовании API в dll C#. Это сделано потому, что целью проекта является преобразование всей библиотеки в управляемый код C#.
Если вы можете получить код для компиляции, вы можете использовать этот тестовый код для воспроизведения проблемы.
Эта проблема
Последний набор изменений под названием moved resource management code to finalizers, to show bug
, показывает оригинальную проблему, которую я имел. Каждый класс в этом коде использует один и тот же шаблон для освобождения неуправляемых ресурсов. Вот пример (C++/CLI):
DBContext::~DBContext()
{
this->!DBContext();
GC::SuppressFinalize(this);
}
DBContext::!DBContext()
{
if(_pst.get() != nullptr)
_pst.reset(); // _pst is a clr_scoped_ptr (managed type)
// that wraps a shared_ptr<T>.
}
Этот код имеет два преимущества. Во-первых, когда такой класс находится в using
Заявление, ресурсы должным образом освобождаются немедленно. Во-вторых, если утилизация забыта пользователем, когда GC наконец решит завершить работу с классом, неуправляемые ресурсы будут освобождены.
Вот проблема с этим подходом, который я просто не могу понять, заключается в том, что иногда GC решает завершить некоторые классы, которые используются для перечисления данных в файле. Это происходит со многими различными файлами PST, и я смог определить, что это как-то связано с вызываемым методом Finalize, хотя класс все еще используется.
Я могу последовательно добиться этого с этим файлом (скачать) 1. Финализатор, который вызывается рано, находится в NodeIdCollection
класс, который находится в файле DBAccessor.cpp. Если вы сможете запустить код, который был связан с вышеуказанным (этот проект может быть сложно настроить из-за зависимостей от библиотеки наддува), приложение завершится с ошибкой, поскольку _nodes
список установлен в нуль и _db_
указатель был сброшен в результате работы финализатора.
1) Есть ли явные проблемы с кодом перечисления в NodeIdCollection
класс, который заставит GC завершить этот класс, пока он еще используется?
Я только смог заставить код работать правильно с обходным путем, который я описал ниже.
Неприглядный обходной путь
Теперь я смог обойти эту проблему, переместив весь код управления ресурсами из каждого из финализаторов (!classname
) деструкторам (~classname
). Это решило проблему, хотя и не решило мое любопытство о том, почему занятия заканчиваются рано.
Тем не менее, есть проблема с подходом, и я признаю, что это больше проблема с дизайном. Из-за интенсивного использования указателей в коде почти каждый класс обрабатывает свои собственные ресурсы и требует удаления каждого класса. Это делает использование перечислений довольно уродливым (C#):
foreach (var msg in pst.Messages)
{
// If this using statement were removed, we would have
// memory leaks
using (msg)
{
// code here
}
}
Оператор using, действующий на элемент в коллекции, просто кричит мне неправильно, однако при таком подходе очень важно предотвратить любые утечки памяти. Без этого dispose никогда не вызывается и память никогда не освобождается, даже если вызывается метод dispose класса pst.
У меня есть все намерения, чтобы изменить этот дизайн. Фундаментальная проблема, когда этот код был написан впервые, помимо того, что я почти ничего не знал о C++/CLI, заключалась в том, что я не мог поместить собственный класс в управляемый. Я чувствую, что возможно было бы использовать указатели с областью действия, которые автоматически освобождают память, когда класс больше не используется, но я не уверен, является ли это правильным способом, или это сработает. Итак, мой второй вопрос:
2) Как лучше всего справиться с неуправляемыми ресурсами в управляемых классах безболезненно?
Чтобы уточнить, могу ли я заменить собственный указатель на clr_scoped_ptr
Оболочка, которая была недавно добавлена в код ( clr_scoped_ptr.h из этого вопроса об обмене стека). Или мне нужно обернуть родной указатель в нечто вроде scoped_ptr<T>
или же smart_ptr<T>
?
Спасибо, что прочитали все это, я знаю, это было много. Я надеюсь, что я достаточно ясно, чтобы я мог получить представление от людей, немного более опытных, чем я. Это такой большой вопрос, я собираюсь добавить вознаграждение, когда оно мне тоже позволяет. Надеюсь, кто-то может помочь.
Спасибо!
1 Этот файл является частью свободно доступного набора данных enron PST-файлов.
2 ответа
clr_scoped_ptr
мой, и приходит отсюда.
Если есть какие-либо ошибки, пожалуйста, дайте мне знать.
Даже если мой код не идеален, использование умного указателя является правильным способом решения этой проблемы, даже в управляемом коде.
Вам не нужно (и не следует) сбрасывать clr_scoped_ptr
в вашем финализаторе. каждый clr_scoped_ptr
сам будет завершен во время выполнения.
При использовании умных указателей вам не нужно писать собственный деструктор или финализатор. Деструктор, сгенерированный компилятором, будет автоматически вызывать деструкторы для всех подобъектов, и каждый финализатор подобъектов будет запускаться после его сбора.
Если присмотреться к вашему коду, то действительно есть ошибка NodeIdCollection
, GetEnumerator()
должен возвращать новый объект перечислителя каждый раз, когда он вызывается, чтобы каждое перечисление начиналось в начале последовательности. Вы повторно используете один перечислитель, то есть эта позиция распределяется между последовательными вызовами GetEnumerator()
, Это плохо.
Освежая мою память о деструкторах / финализаторах, из некоторой документации Microsoft, я думаю, вы могли бы, по крайней мере, немного упростить свой код.
Вот моя версия вашей последовательности:
DBContext::~DBContext()
{
this->!DBContext();
}
DBContext::!DBContext()
{
delete _pst;
_pst = NULL;
}
"GC::SupressFinalize" автоматически выполняется C++/CLI, поэтому в этом нет необходимости. Поскольку переменная _pst инициализируется в конструкторе (и удаление пустой переменной в любом случае не вызывает проблем), я не вижу причин усложнять код с помощью интеллектуальных указателей.
Что касается отладочной заметки, мне интересно, можете ли вы помочь сделать проблему более очевидной, добавив несколько вызовов "GC::Collect". Это должно заставить вас завершить работу над висящими объектами.
Надеюсь, это немного поможет,