У меня проблемы с финализаторами, которые, по-видимому, вызываются в начале проекта C++/CLI (и C#), над которым я работаю. Это кажется очень сложной проблемой, и я собираюсь упомянуть много разных классов и типов из кода. К счастью, у него открытый исходный код, и вы можете следить за ним здесь: Pstsdk.Net (mercurial-репозиторий). пытался напрямую ссылаться на браузер файлов, где это уместно, чтобы вы могли просматривать код по мере чтения. Большая часть кода, с которым мы имеем дело, находится в папке 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
показывает исходную проблему, с которой я столкнулся. Каждый класс в этом коде использует один и тот же шаблон для освобождения неуправляемых ресурсов. Вот пример (С++/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, наконец, решит завершить класс, неуправляемые ресурсы будут освобождены.
Вот проблема с этим подходом, которую я просто не могу понять, заключается в том, что иногда сборщик мусора решает завершить некоторые классы, которые используются для перечисления данных в файле. Это происходит со многими различными файлами PST, и я смог определить, что это как-то связано с вызываемым методом Finalize, хотя класс все еще используется.
Я всегда могу добиться этого с помощью этого файла (скачать)1. Финализатор, который вызывается раньше, находится в классе NodeIdCollection
, который находится в файл DBAccessor.cpp. Если вы сможете запустить код, указанный выше (этот проект может быть сложно настроить из-за зависимостей от библиотеки boost), приложение завершится ошибкой с исключением, так как список _nodes
имеет значение null, а _db_
указатель был сброшен в результате работы финализатора.
1) Существуют ли какие-либо явные проблемы с кодом перечисления в классе NodeIdCollection
, из-за которых сборщик мусора завершает работу над этим классом, пока он еще используется?
Я смог заставить код работать правильно только с помощью обходного пути, который я описал ниже.
Неприглядный обходной путь
Теперь я смог обойти эту проблему, переместив весь код управления ресурсами из финализаторов (!classname
) в деструкторы (~classname
). Это решило проблему, хотя и не развеяло моего любопытства по поводу того, почему классы завершаются раньше.
Однако есть проблема с подходом, и я признаю, что это больше проблема с дизайном. Из-за интенсивного использования указателей в коде почти каждый класс обрабатывает свои собственные ресурсы и требует удаления каждого класса. Это делает использование перечислений довольно уродливым (С#):
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