Подтвердить что ты не робот

Исключение NullReferenceException в финализаторе во время MSTest

(Я знаю, это смехотворно длинный вопрос. Я пытался отделить вопрос от моего исследования до сих пор, поэтому его немного легче читать.)

Я запускаю свои модульные тесты с помощью MSTest.exe. Иногда я вижу эту ошибку теста:

В индивидуальном методе unit test: "Процесс агента был остановлен во время выполнения теста".

На весь тестовый прогон:

One of the background threads threw exception: 
System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Runtime.InteropServices.Marshal.ReleaseComObject(Object o)
   at System.Management.Instrumentation.MetaDataInfo.Dispose()
   at System.Management.Instrumentation.MetaDataInfo.Finalize()

Итак, вот что мне нужно сделать: мне нужно отследить, что вызывает ошибку в MetaDataInfo, но я рисую пробел. Мой пакет unit test занимает более получаса, и ошибка не возникает каждый раз, поэтому трудно воспроизвести его.

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

Edit:

Проверяемый код представляет собой сочетание С#, С++/CLI и немного неуправляемого кода на С++. Неуправляемый С++ используется только из С++/CLI, никогда непосредственно из модульных тестов. Модульные тесты - все С#.

Тестирование кода будет выполняться в автономной службе Windows, поэтому никаких осложнений от ASP.net или чего-либо подобного нет. В тестируемом коде запускаются и останавливаются потоки, сетевая связь и ввод/вывод файлов на локальный жесткий диск.


Мое расследование до сих пор:

Я потратил некоторое время на копирование нескольких версий сборки System.Management на моей машине с Windows 7, и я нашел класс MetaDataInfo в System.Management в моем каталоге Windows. (Версия, которая в разделе Program Files\Reference Assemblies намного меньше, и не имеет класса MetaDataInfo.)

Используя Reflector для проверки этой сборки, я нашел то, что кажется очевидной ошибкой в ​​MetaDataInfo.Dispose():

// From class System.Management.Instrumentation.MetaDataInfo:
public void Dispose()
{
    if (this.importInterface == null) // <---- Should be "!="
    {
        Marshal.ReleaseComObject(this.importInterface);
    }
    this.importInterface = null;
    GC.SuppressFinalize(this);
}

С помощью этого оператора "if" в обратном направлении MetaDataInfo пропустит COM-объект, если он есть, или выбросит исключение NullReferenceException, если нет. Я сообщил об этом в Microsoft Connect: https://connect.microsoft.com/VisualStudio/feedback/details/779328/

Используя отражатель, я смог найти все виды использования класса MetaDataInfo. (Это внутренний класс, поэтому просто поиск сборки должен быть полным списком.) Используется только одно место:

public static Guid GetMvid(Assembly assembly)
{
    using (MetaDataInfo info = new MetaDataInfo(assembly))
    {
        return info.Mvid;
    }
}

Поскольку все использования MetaDataInfo корректно устранены, вот что происходит:

  • Если MetaDataInfo.importInterface не имеет значения null:
    • статический метод GetMvid возвращает MetaDataInfo.Mvid
    • using вызывает MetaDataInfo.Dispose
      • Утилизировать утечку COM-объекта
      • Dispose устанавливает importInterface в null
      • Утилизировать вызовы GC.SuppressFinalize
    • Позже, когда GC собирает MetaDataInfo, финализатор пропускается.
  • .
  • Если MetaDataInfo.importInterface имеет значение null:
    • статический метод GetMvid получает исключение NullReferenceException, вызывающее MetaDataInfo.Mvid.
    • Прежде чем распространение распространяется, using вызывает MetaDataInfo.Dispose
      • Утилизировать вызовы Marshal.ReleaseComObject
        • Marshal.ReleaseComObject создает исключение NullReferenceException.
      • Поскольку выбрано исключение, Dispose не вызывает GC.SuppressFinalize
    • Исключение распространяется до получателя GetMvid.
    • Позже, когда GC собирает MetaDataInfo, он запускает Finalizer
      • Завершение вызовов Dispose
        • Утилизировать вызовы Marshal.ReleaseComObject
          • Marshal.ReleaseComObject генерирует исключение NullReferenceException, которое распространяется до GC, и приложение завершается.

Для чего это стоит, вот остальная часть соответствующего кода из MetaDataInfo:

public MetaDataInfo(string assemblyName)
{
    Guid riid = new Guid(((GuidAttribute) Attribute.GetCustomAttribute(typeof(IMetaDataImportInternalOnly), typeof(GuidAttribute), false)).Value);
    // The above line retrieves this Guid: "7DAC8207-D3AE-4c75-9B67-92801A497D44"
    IMetaDataDispenser o = (IMetaDataDispenser) new CorMetaDataDispenser();
    this.importInterface = (IMetaDataImportInternalOnly) o.OpenScope(assemblyName, 0, ref riid);
    Marshal.ReleaseComObject(o);
}

private void InitNameAndMvid()
{
    if (this.name == null)
    {
        uint num;
        StringBuilder szName = new StringBuilder {
            Capacity = 0
        };
        this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid);
        szName.Capacity = (int) num;
        this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid);
        this.name = szName.ToString();
    }
}

public Guid Mvid
{
    get
    {
        this.InitNameAndMvid();
        return this.mvid;
    }
}

Изменить 2:

Я смог воспроизвести ошибку в классе MetaDataInfo для Microsoft. Однако мое воспроизведение немного отличается от того, что я вижу здесь.

  • Воспроизведение: Я пытаюсь создать объект MetaDataInfo в файле, который не является управляемой сборкой. Это генерирует исключение из конструктора до инициализации importInterface.
  • Моя проблема с MSTest: MetaDataInfo построена на некоторой управляемой сборке, и что-то происходит, чтобы сделать importInterface null или выйти из конструктора до того, как importInterface будет инициализирован.
    • Я знаю, что MetaDataInfo создается на управляемой сборке, потому что MetaDataInfo является внутренним классом, и единственный API, который его вызывает, делает это, передавая результат Assembly.Location.

Однако повторное создание проблемы в Visual Studio означало, что она загрузила источник в MetaDataInfo для меня. Вот реальный код, с оригинальными комментариями разработчиков.

public void Dispose()
{ 
    // We implement IDisposable on this class because the IMetaDataImport
    // can be an expensive object to keep in memory. 
    if(importInterface == null) 
        Marshal.ReleaseComObject(importInterface);
    importInterface = null; 
    GC.SuppressFinalize(this);
}

~MetaDataInfo() 
{
    Dispose(); 
} 

Исходный код подтверждает то, что было замечено в рефлекторе: оператор if обратный, и они не должны получать доступ к управляемому объекту из Finalizer.

Я уже говорил об этом, потому что он никогда не вызывал ReleaseComObject, что он пропускал COM-объект. Я читал больше об использовании COM-объектов в .Net, и если я правильно его понимаю, это было неправильно: COM-объект не был выпущен при вызове Dispose(), но он освобождается, когда сборщик мусора приближается к сборщик Runtime Callable Wrapper, который является управляемым объектом. Несмотря на то, что это оболочка для неуправляемого COM-объекта, RCW по-прежнему является управляемым объектом, и правило "не обращаться к управляемым объектам из финализатора" должно применяться.

4b9b3361

Ответ 1

Попробуйте добавить следующий код в определение класса:

bool _disposing = false  // class property

public void Dispose()
{
    if( !disposing ) 
        Marshal.ReleaseComObject(importInterface);
    importInterface = null; 
    GC.SuppressFinalize(this);

    disposing = true;
}

Ответ 2

Если MetaDataInfo использует шаблон IDisposable, тогда также должен быть финализатор (~ MetaDataInfo() в С#). Оператор using обязательно вызовет Dispose(), который устанавливает для параметра importInterface значение null. Затем, когда GC готов к завершению, вызывается MetaDataInfo(), который обычно вызывает Dispose (или, скорее, перегрузку с использованием bool disposing: Dispose (false)).

Я бы сказал, что эта ошибка должна появляться довольно часто.

Ответ 3

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

Вкратце (любил ваше исследование), вы заявляете, что Dispose вызывает Finalize. Это наоборот. Завершите, когда вызывается вызовом GC Dispose.