Это использование 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 будет равен null. Если я запустил приложение из сборки Debug (с присоединенным или отсутствующим отладчиком), то validationResultsUsingRecomputedExtrinsics будет содержать достоверные данные.

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

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

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

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

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

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

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

Ответ 1

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

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

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

Ответ 2

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

Обновление DG: удаление, завершение и управление ресурсами

Хорошо! Вот он: пересмотренная запись "Руководство по проектированию, устранению, завершению и управлению ресурсами". Я упомянул эту работу ранее здесь и здесь. На ~ 25 печатных страницах это не то, что я считаю небольшим обновлением. Принял меня намного дольше, чем ожидалось, но я доволен результатом. Я получил работу и получил большое количество отзывов от HSutter, BrianGru, CBrumme, Jeff Richter и еще пары людей на этом... Хорошая забава.

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

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

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

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

Ответ 3

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

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

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() он почти как если бы он работал в основном потоке.)