Ранняя финализация и утечки памяти в библиотеке C++/CLI

У меня проблемы с финализаторами, которые, по-видимому, вызываются в начале проекта 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


person Christopher Currens    schedule 13.10.2011    source источник
comment
Я серьезно сомневаюсь, что финализатор вызывается потоком финализации .NET, если объект все еще используется. Можете ли вы сузить код до очень простого примера, демонстрирующего такое поведение?   -  person Lasse V. Karlsen    schedule 16.10.2011
comment
@LasseV.Karlsen - я, конечно, могу попробовать, хотя я не уверен, насколько это будет просто из-за обернутого кода, интенсивно использующего библиотеку boost, я полагаю, что мне, возможно, придется включить это, чтобы воспроизвести эту проблему. сам. Я буду стараться изо всех сил, хотя.   -  person Christopher Currens    schedule 16.10.2011
comment
@LasseV.Karlsen - я пытаюсь воспроизвести это (пока безуспешно), но я хотел решить одну вещь. Приведенный выше код показывает, что завершение происходит, когда объект все еще используется. Я могу поставить точку останова в финализаторе коллекции, когда я ее перечисляю. Примерно на полпути, когда еще предстоит пройти, точка останова в финализаторе срабатывает. Интересно то, что я все еще могу получить доступ к объекту, но при запуске финализатора внутренние объекты удаляются в соответствии с моим кодом. Я ожидал бы ObjectDisposedException?   -  person Christopher Currens    schedule 16.10.2011
comment
Похоже, вы нарушаете мои авторские права, потому что не выполнили мои (очень щедрые) лицензионные условия. Это можно исправить, отредактировав заявление об авторских правах по адресу pstsdknet.codeplex.com/SourceControl/changeset/view/   -  person Ben Voigt    schedule 16.10.2011
comment
@BenVoigt - я добавлю это. Я убедился, что авторские права сохранены в исходном коде, но я забыл сделать это для двоичного файла. Это в новом наборе изменений.   -  person Christopher Currens    schedule 16.10.2011
comment
@ChristopherCurrens: Спасибо, я очень ценю хоть какое-то признание кода, поскольку это единственная форма компенсации, которую я получу.   -  person Ben Voigt    schedule 16.10.2011


Ответы (2)


clr_scoped_ptr принадлежит мне и взято из здесь.

Если у него есть какие-либо ошибки, пожалуйста, дайте мне знать.

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

Вам не нужно (и не следует) сбрасывать clr_scoped_ptr в финализаторе. Каждый clr_scoped_ptr будет завершен средой выполнения.

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


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

person Ben Voigt    schedule 15.10.2011
comment
@BenVoight - Понятия не имею о минусах, но спасибо! Это действительно была проблема со счетчиком. NodeIdCollection делился перечислителем, он выходил за рамки и собирался GC, и удалял перечислитель, хотя я все еще его использовал. По какой-то причине я ожидал, что ссылка ^enumerator предотвратит это, но, как вы указали, в любом случае это было неправильно. - person Christopher Currens; 17.10.2011
comment
@BenVoight - я решил вместо отрицательного голоса, времени, которое вы потратили на просмотр кода, чтобы помочь, и того факта, что ваш clr_scoped_ptr работает так хорошо, я хотел бы дать вам дополнительные 100 повторений, что я и сделаю когда он позволяет мне в течение 24 часов. - person Christopher Currens; 17.10.2011
comment
@ChristopherCurrens: Не волнуйся. Людям, которые оставляют минусы без объяснения причин, не стоит беспокоиться. Я просто оставляю этот комментарий в надежде, что избиратель придет и объяснится. - person Ben Voigt; 18.10.2011

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

Вот моя версия вашей последовательности:

DBContext::~DBContext()
{
    this->!DBContext();
}

DBContext::!DBContext()
{
    delete _pst;
    _pst = NULL;
}

GC::SupressFinalize автоматически выполняется C++/CLI, поэтому в этом нет необходимости. Поскольку переменная _pst инициализируется в конструкторе (и удаление нулевой переменной в любом случае не вызывает проблем), я не вижу причин усложнять код с помощью умных указателей.

На заметку об отладке: интересно, можете ли вы помочь сделать проблему более очевидной, добавив несколько вызовов GC::Collect. Это должно принудительно завершить работу с оборванными объектами.

Надеюсь, это немного поможет,

person Adrian Conlon    schedule 15.10.2011
comment
Я понятия не имею, что такое clr_scope_ptr. Но этот код легко удаляет один и тот же объект более одного раза, когда клиентский код вызывает Dispose() более одного раза. Не полезно. - person Hans Passant; 16.10.2011
comment
Хороший вопрос, редактирование помогает? Мой исходный пост был основан на неопределенности clr_scope_ptr и попытке его удалить... - person Adrian Conlon; 16.10.2011
comment
Не совсем, не компилируется. Используйте нольптр. - person Hans Passant; 16.10.2011
comment
Хм, я предполагал, что _pst был указателем на неуправляемый ресурс. Как вы думаете, это управляемый ресурс? - person Adrian Conlon; 16.10.2011
comment
Нет, я уверен, что он неуправляемый. Однако код скомпилирован с /clr. Что запрещает NULL, направляя С++ 11 на 6 лет раньше срока. Используйте 0 или nullptr. Всегда лучше сначала попытаться скомпилировать код, который вы публикуете :) - person Hans Passant; 16.10.2011
comment
@ХансПассант: #define NULL (0). Так что, если вы говорите, что 0 или nullptr работают, то очевидно, что NULL тоже работает. - person Ben Voigt; 16.10.2011