Такое использование GC.SuppressFinalize () кажется неправильным

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

Функциональный код (после отладки проблемы с поставщиком) примерно следующий:

    Task.Factory.StartNew(() => ValidateCalibration(pelRectRaw2Ds, crspFeatures, Calibration.Raw2DFromPhys3Ds));

    .....

    private void ValidateCalibration(List<Rectangle> pelRectRaw2Ds, List<List<3DCrspFeaturesCollection>> crspFeatures, List<3DCameraCalibration> getRaw2DFromPhys3Ds)
    {
        var calibrationValidator = new 3DCameraCalibrationValidator();

        // This is required according to vendor otherwise validationResultsUsingRecomputedExtrinsics is occasionally null after preforming the validation
        GC.SuppressFinalize(calibrationValidator);

        3DCameraCalibrationValidationResult validationResultUsingOriginalCalibrations;
        3DCameraCalibrationValidationResult validationResultsUsingRecomputedExtrinsics;
        calibrationValidator.Execute(pelRectRaw2Ds, crspFeatures, getRaw2DFromPhys3Ds, out validationResultUsingOriginalCalibrations, out validationResultsUsingRecomputedExtrinsics);

        Calibration.CalibrationValidations.Add(new CalibrationValidation
            {
                Timestamp = DateTime.Now,
                UserName = Globals.InspectionSystemObject.CurrentUserName,
                ValidationResultUsingOriginalCalibrations = validationResultUsingOriginalCalibrations,
                ValidationResultsUsingRecomputedExtrinsics = validationResultsUsingRecomputedExtrinsics
            });
    }

Процесс проверки - довольно трудоемкая операция, поэтому я передаю ее Задаче. Проблема, с которой я столкнулся, заключалась в том, что изначально у меня не было вызова GC.SuppressFinalize (CalibrationValidator), и когда приложение запускалось из сборки Release, тогда выходной параметр validationResultsUsingRecomputedExtrinsics был бы нулевым. Если бы я запустил приложение из отладочной сборки (с прикрепленным отладчиком или без него), тогда validationResultsUsingRecomputedExtrinsics будет содержать действительные данные.

Я не совсем понимаю, что GC.SuppressFinalize () сделал в этой ситуации или как он устранил проблему. Все, что я могу найти относительно GC.SuppressFinalize (), - это то, что он используется при реализации IDisposable. Я не могу найти ему никакого применения в "стандартном" коде.

Как / почему добавление вызова к GC.SuppressFinalize (CalibrationValidator) решает эту проблему?

Я понимаю, что без глубоких знаний о внутреннем устройстве библиотеки поставщика, возможно, будет невозможно узнать наверняка, но любое понимание может помочь.

Приложение скомпилировано с помощью VS2012, ориентированного на .NET 4.0. Эта библиотека поставщика требует, чтобы параметр useLegacyV2RuntimeActivationPolicy = "true" был указан в app.config.

Это обоснование, которое я получил от продавца:

Команда SuppressFinalize гарантирует, что сборщик мусора не очистит что-то «раньше». Похоже, что по какой-то причине ваше приложение иногда заставляло сборщика мусора немного усердствовать и очищать объект до того, как вы действительно закончили с ним; это почти наверняка связано с областью видимости и, возможно, из-за многопоточности, вызывающей путаницу в области калибровки Validator. Ниже я получил ответ от инженерного отдела.

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


person Dave Nay    schedule 22.03.2013    source источник
comment
Мне кажется, это запах кода.   -  person Bernard    schedule 22.03.2013
comment
@Bernard Я согласен ... поэтому я ищу разъяснения.   -  person Dave Nay    schedule 22.03.2013
comment
Если это больше подходит для StackOverflow, ничего страшного (я не собираюсь его дублировать, пожалуйста, перенесите)   -  person Dave Nay    schedule 22.03.2013
comment
Я думаю, что это, скорее всего, вызвано ошибкой в ​​финализаторе 3DCameraCalibrationValidator (потому что авторы не ожидали, что финализатор может работать во время выполнения Execute()). Использование SuppressFinalize() позволяет обойти эту ошибку.   -  person svick    schedule 22.03.2013


Ответы (3)


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

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

Правильное исправление с вашей стороны - убедиться, что дрожание помечает объект, который используется после вызова, чтобы сборщик мусора не собирал его. Для этого нужно добавить GC.KeepAlive(calibrationValidator) после Execute() вызова.

person Hans Passant    schedule 22.03.2013
comment
Спасибо, Ганс. В своем первоначальном вопросе я должен был указать, что утечка ресурсов вызывает дополнительную озабоченность. - person Dave Nay; 22.03.2013

Когда дело доходит до понимания IDisposable, GC.SuppressFinalize и финализаторов в C #, я не думаю, что существует лучшее объяснение, чем следующая статья.

Обновление DG: удаление, финализация и Управление ресурсами

Хорошо! Вот она: пересмотренная запись Руководства по проектированию «Удаление, завершение и управление ресурсами». Я уже упоминал об этой работе ранее здесь и . При ~ 25 печатных страницах это не то, что я считаю незначительным обновлением. У меня ушло намного больше времени, чем предполагалось, но результатом я доволен. Мне пришлось поработать и получить хорошие отзывы от HSutter, BrianGru, CBrumme, Джеффа Рихтера и пары других людей по этому поводу ... Хорошее развлечение.

Ключевая концепция этого вопроса:

Это настолько очевидно, что GC.SuppressFinalize() следует вызывать только на this, что в статье это даже не упоминается напрямую. Однако в нем упоминается практика обертывания финализируемых объектов, чтобы изолировать их от общедоступного API, чтобы гарантировать, что внешний код не может вызывать GC.SuppressFinalize() на этих ресурсах (см. Следующую цитату). Кто бы ни разработал библиотеку, описанную в исходном вопросе, не понимает, как работает доработка в .NET.

Цитата из статьи в блоге:

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

person Sam Harwell    schedule 22.03.2013
comment
Кто бы ни разработал библиотеку, описанную в исходном вопросе, не понимает, как работает доработка в .NET. Я бы в это поверил. В первую очередь это компания C ++, и я уверен, что их библиотека .NET является лишь оболочкой для их библиотеки C ++. Я добавил их ответ на свой вопрос, который, кажется, поддерживает идею о том, что это плохо реализованная библиотека. - person Dave Nay; 22.03.2013
comment
@DaveNay Не стесняйтесь присылать им мою контактную информацию (tunnelvisionlabs.com). Мы помогли более чем одной команде упорядочить детали своей финальной версии, особенно когда задействовано управляемое / собственное взаимодействие. :) - person Sam Harwell; 22.03.2013
comment
Они должны использовать GC.KeepAlive (this) в конце этой функции Execute, чтобы предотвратить this сборку мусора и завершение слишком рано. В качестве обходного пути вы можете использовать GC.KeepAlive (calibrationValidator) после вызова Execute. - person Anton Tykhyy; 22.03.2013

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

Рассмотрим следующую программу:

using System;

class Program
{
    private static void Main()
    {
        var outer = new Outer();
        Console.WriteLine(outer.GetValue() == null);
    }
}

class Outer
{
    private Inner m_inner = new Inner();

    public object GetValue()
    {
        return m_inner.GetValue();
    }

    ~Outer()
    {
        m_inner.Dispose();
    }
}

class Inner
{
    private object m_value = new object();

    public object GetValue()
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        return m_value;
    }

    public void Dispose()
    {
        m_value = null;
    }
}

Здесь, пока вызывается outer.GetValue(), outer будет собираться и завершаться сборщиком мусора (по крайней мере, в режиме Release). Финализатор обнуляет поле объекта Inner, что означает, что GetValue() вернет null.

В реальном коде у вас, скорее всего, не будет вызовов GC. Вместо этого вы должны создать некоторый управляемый объект, который (недетерминированно) вызывает запуск сборщика мусора.

(Я сказал, что этот код в основном однопоточный. Фактически, финализатор будет работать в другом потоке, но из-за вызова WaitForPendingFinalizers() он почти как если бы он работал в основном потоке.)

person svick    schedule 22.03.2013