Безопасно утилизируйте в финализаторе .net

Я хотел найти способ разорвать цепочку IDisposable, где какой-то вложенный класс, от которого вы внезапно зависите, теперь реализует IDisposable, и вы не хотите, чтобы этот интерфейс волновал слои вашего композита. По сути, у меня слабые подписки на IObservable<T> через 'SubscribeWeakly()', который я хочу очистить, когда уйду, чтобы не допустить утечки экземпляров оболочки на тот случай, если Observable никогда не сработает. Это была мотивация, но я использую ее и для других целей.

В другом сообщении была аналогичная проблема, и ответ в основном указано, что вы все еще можете получить доступ к одноразовым элементам в вашем финализаторе. Однако вам не гарантируется, в каком порядке запускаются финализаторы, поэтому удаление может быть проблематичным.

Поэтому мне нужен был способ гарантировать, что одноразовый элемент останется живым, чтобы я мог вызвать Dispose() в моем финализаторе. Итак, я посмотрел на GCHandle, который позволяет C++ удерживать (и поддерживать в рабочем состоянии) управляемые объекты, втягивая их и их агрегаты в дескриптор приложения, чтобы поддерживать их в рабочем состоянии до тех пор, пока дескриптор не будет освобожден, а составное время жизни не вернется под контроль .NET. менеджер памяти. Исходя из C++, я подумал, что поведение, подобное std::unique_ptr, будет хорошим, поэтому я придумал что-то похожее на AutoDisposer.

public class AutoDisposer
{
    GCHandle _handle;

    public AutoDisposer(IDisposable disposable)
    {
        if (disposable == null) throw new ArgumentNullException();

        _handle = GCHandle.Alloc(disposable);
    }

    ~AutoDisposer()
    {
        try 
        {
            var disposable = _handle.Target as IDisposable;
            if (disposable == null) return;
            try 
            {
                disposable.Dispose();
            }
            finally 
            {
                _handle.Free();
            }
        }
        catch (Exception) { }
    }
}

В классе, которому нужно распоряжаться ресурсами, когда он уходит, я бы просто назначил поле, например _autoDisposables = new AutoDisposer(disposables). Затем этот AutoDisposer будет очищен сборщиком мусора вокруг них в то же время, что и содержащий его класс. Тем не менее, мне интересно, какие проблемы могут быть с этой техникой. Прямо сейчас я могу думать о следующем:

  • Дополнительные накладные расходы на сборщик мусора из-за наличия финализаторов
  • Дополнительные накладные расходы, связанные с тем, что .NET приходится извлекать элементы из управляемой памяти в дескрипторы приложений и возвращать их.
  • Не подлежит модульному тестированию (я не могу предсказать, когда ресурс будет возвращен в .NET для управления памятью).

Поэтому я использую его экономно, когда реализация IDisposable не слишком обременительна, если мне нужно детерминистически вызывать Dispose() и т. д.

Кто-нибудь видит другие проблемы? Эта техника вообще действительна?


person moes    schedule 01.08.2013    source источник
comment
Простой ответ: удалить все деструкторы (финализаторы). Забудь об этом.   -  person Henk Holterman    schedule 01.08.2013
comment
Мне нужен был способ гарантировать, что одноразовые предметы останутся в живых, чтобы я мог позвонить Dispose() — это переворачивает все с ног на голову.   -  person Henk Holterman    schedule 01.08.2013
comment
К тому времени, когда вы находитесь внутри вашего финализатора, IDisposable (если он реализован правильно) может уже запустить свой финализатор и выполнить столько очистки, сколько можно разумно ожидать. внутри финализатора. Ваш вызов Dispose на этом этапе вряд ли поможет.   -  person Damien_The_Unbeliever    schedule 01.08.2013
comment
Я предполагаю, что проблема в том, что удаление подписки на самом деле просто выполняет управляемое действие (здесь не нужно беспокоиться о неуправляемых ресурсах). Действие состоит в том, чтобы отменить регистрацию слушателя (или экземпляра оболочки слушателя в моем случае) для события. Этого не произойдет, если событие никогда не срабатывает и Dispose никогда не вызывается. Так что я должен вызвать Dispose из моего финализатора. Я поддерживал его в рабочем состоянии, чтобы гарантировать отсутствие проблем при вызове Dispose (висячие нулевые указатели, условия гонки и т. д.).   -  person moes    schedule 02.08.2013


Ответы (2)


Я думаю, вы могли неправильно понять IDispoable - типичный шаблон для объекта IDisposable примерно таков:

void Dispose() { Dispose(true); }

void Dispose(bool disposing)
{
    if (disposing)
    {
        // Free managed resources
    }
    // always free unmanaged resources
}

~Finalizer() { Dispose (false); }

Поскольку финализатор всегда должен обрабатывать неуправляемые ресурсы, если вы ждете его запуска (что произойдет в какой-то момент в будущем, когда ограничения памяти вызовут сборку мусора или она будет запущена вручную), утечки не должно быть. Если вы хотите быть детерминированным в отношении того, когда эти ресурсы освобождаются, вам придется выставлять IDispoable вниз по иерархии классов.

person Rowland Shaw    schedule 01.08.2013
comment
Конечно, желательно правильное использование Dispose. Вопрос, на мой взгляд, заключается в том, как ограничить ущерб, наносимый кодом, который не вызывает Dispose. Например, у вас может быть класс ведения журнала с функцией регистрации в предварительном порядке, которая будет буферизовать данные, которые должны быть зарегистрированы в случае сбоя. В случае успеха он отбрасывает этот буфер и регистрирует успех; в противном случае он зарегистрирует ошибку вместе с содержимым буфера. Если такой объект заброшен, может потребоваться, чтобы он выводил свой буфер вместе с записью о том, что он был заброшен, прежде чем файл журнала будет закрыт. - person supercat; 02.08.2013
comment
Кажется, я понимаю шаблон Dispose. @supercat изобразил мои намерения. Я пытаюсь ограничить ущерб, наносимый кодом, который не вызывает Dispose. Нет неуправляемых ресурсов, с которыми нужно иметь дело. Dispose выполняет действие (удаление оболочки события из события IObservable в Rx), которое не произойдет, если я не вызову Dispose. Если источник события исчезнет, ​​я в порядке. Если это не так, мне нужно отменить регистрацию моей слабой (слабой, что означает, что она не имеет сильной ссылки на меня) оболочки подписки, чтобы избежать ее утечки. Так что мне просто нужен безопасный способ вызова Dispose, когда я ухожу без реализации IDisposable - person moes; 02.08.2013
comment
Дело в том, что Dispose предназначен для детерминированной очистки ресурсов. Если дело дошло до вызова вашего финализатора, вы могли уже быть освобождены к тому времени, так как сборщик мусора считает, что ничто важное не ссылается на вас - определяя финализатор, вы заставите объект зависнуть вокруг немного дольше, пока он находится в очереди Finalizer. - person Rowland Shaw; 02.08.2013

Можно спроектировать классы так, чтобы они могли координировать свое поведение при завершении друг с другом. Например, объект может принимать параметр конструктора типа Action(bool) и указывать, что, если он не равен нулю, он будет вызываться в качестве первого шага Dispose(bool) [поддерживающее поле может быть прочитано с помощью Interlocked.Exchange(ref theField, null), чтобы гарантировать, что делегат будет вызван не более одного раза ]. Если класс, который, например. инкапсулирует файл, содержащий такую ​​функцию, и был обернут в класс, который инкапсулирует файл с дополнительной буферизацией, файл уведомит класс буферизации о том, что он собирается закрыться, и, таким образом, класс буферизации может гарантировать, что все необходимые данные будут записаны. К сожалению, такой шаблон не распространен во фреймворке.

Учитывая отсутствие такого шаблона, единственный способ, с помощью которого класс, инкапсулирующий буферизованный файл, может гарантировать, что ему удастся записать свои данные, если он будет заброшен, без закрытия файла до того, как он сможет это сделать, состоит в том, чтобы сохранить статическую ссылку где-нибудь на файл (возможно, используя статический экземпляр ConcurrentDictionary(bufferedWrapper, fileObject)) и убедитесь, что при очистке он уничтожит эту статическую ссылку, запишет свои данные в файл, а затем закроет файл. Обратите внимание, что этот подход следует использовать только в том случае, если объект-оболочка сохраняет исключительный контроль над объектом, который он обертывает, и требует особого внимания к деталям. Финализаторы имеют много странных угловых случаев, трудно правильно обработать их все, и любая неспособность правильно обработать неясные угловые случаи, вероятно, приведет к Heisenbugs.

PS Продолжая подход ConcurrentDictionary, если вы используете что-то вроде событий, ваши основные проблемы могут заключаться в том, чтобы (1) гарантировать, что если объект заброшен, события не содержат ссылку на что-либо " большой"; (2) обеспечение того, чтобы количество заброшенных объектов в ConcurrentDictionary не могло расти без ограничений. Первую проблему можно решить, обеспечив отсутствие «сильного» пути ссылки от события к какому-либо значимому «лесу» взаимосвязанных объектов; если лишняя подписка содержит только ссылки на объекты размером около 100 байт, и они будут очищены, если какое-либо из событий когда-либо сработает, даже тысяча заброшенных подписок будет представлять собой очень незначительную проблему [при условии, что число ограничено]. Вторую проблему можно решить, если каждый запрос на подписку будет опрашивать некоторые элементы в словаре (либо по запросу, либо по амортизированной основе), чтобы увидеть, не заброшены ли они, и очистить их, если это так. Если некоторые события заброшены и никогда не срабатывают, и если никогда не добавляются новые события этого типа, эти события могут оставаться на неопределенный срок, но они будут безвредны. Единственным способом, которым события могут быть значительно вредными, было бы, если бы они содержали ссылки на большие объекты (чего можно избежать, используя слабые ссылки), неограниченное количество событий можно было бы добавлять и отбрасывать без какой-либо очистки (чего не произойдет, если добавление новых событий приводит к очистке заброшенных), или если такие события могут непрерывно тратить процессорное время (чего не произойдет, если первая попытка запустить их после того, как объекты, которые заботились о них, исчезли, приведет к их очистке ).

person supercat    schedule 01.08.2013
comment
Разве мой GCHandle не гарантирует, что одноразовый объект останется в живых, чтобы безопасно вызывать его dispose после завершения? Это то, что используется для C++, чтобы гарантировать, что управляемые объекты поддерживаются собственным кодом. Проблемы могут возникнуть при закрытии приложения. Однако они, похоже, не для C++, поэтому я предполагаю, что Microsoft решила эту проблему (возможно, .NET уже закрыт, поэтому дескриптор приложения просто освобождает память и включает любые неуправляемые ресурсы, как обычно). - person moes; 02.08.2013
comment
Я подумал о подходе ConcurrentDictionary с потоком, который будет выполняться очень часто, чтобы очистить его (вызов dispose для элементов, привязанных к времени жизни других элементов — с использованием слабых ссылок). Не было бы финализатора, чтобы беспокоиться об удалении вещей при завершении работы приложения. Однако для этого потребуется дополнительный поток, глобальная переменная (словарь) и установление времени для всего приложения для его очистки. Подход AutoDisposer казался более автономным. - person moes; 02.08.2013