IDisposable с участником от отсутствующей внешней сборки терпит неудачу в финализаторе - PullRequest
1 голос
/ 24 сентября 2011

У меня есть 2 сборки, A, содержащая метод Main и класс Foo, который использует класс Bar из сборки B:

Сборка прутка (сборка B):

public sealed class Bar : IDisposable { 
    /* ... */ 
    public void Dispose() { /* ... */ }
}

Класс Foo (сборка A):

public class Foo : IDisposable {
    private readonly Bar external;
    private bool disposed;
    public Foo()
    { 
        Console.WriteLine("Foo");
        external = new Bar(); 
    }
    ~Foo()
    { 
        Console.WriteLine("~Foo");
        this.Dispose(false); 
    }
    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool disposing)
    {
        if (disposed) return;
        if (disposing) external.Dispose();
        disposed = true;
    }
}

Точка входа (в сборке A):

class Program
{
    static void Main(string[] args)
    {
        try
        {
            var foo = new Foo();
            Console.WriteLine(foo);
        }
        catch (FileNotFoundException ex) 
        {
            // handle exception
            Console.WriteLine(ex.ToString());
        }
        Console.ReadLine();
    }
}

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

Поэтому, когда я удаляю сборку B и запускаю приложение, я ожидаю, что блок try catch в методе main обрабатывает исключение FileNotFoundException, возникающее при отсутствии сборки B. Что-то вроде того, но проблемы начинаются ...

Когда приложение продолжается (вводится строка в консоли), вызывается финализатор класса Foo (?!), Хотя экземпляр Foo не был создан - конструктор не был вызван. Поскольку экземпляра класса нет, у меня нет возможности вызвать GC.SupressFinalize для этого экземпляра извне. Единственное, что вы видите в выводе консоли при запуске проекта без сборки B, это ~ Foo .

Итак, вопросы:

  • Почему вызывается финализатор, хотя экземпляр класса не создается? (для меня это абсолютно бессмысленно! Я бы хотел быть просветленным)
  • Можно ли предотвратить сбой приложения без блока try-catch в финализаторе? (это будет означать рефакторинг всей базы кода ...)

Некоторые предыстории: Я столкнулся с этой проблемой при написании корпоративного приложения включения плагина с требованием, чтобы оно продолжало работу, если в папке развертывания плагина отсутствует dll и отмечен неисправный плагин. Я подумал, что блока try-catch вокруг процедуры загрузки внешнего плагина будет достаточно, но, очевидно, это не так, так как после перехвата первого исключения все еще вызывается финализатор (в потоке GC), что в конечном итоге приводит к сбою приложения. *

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

Замечание 2 Если я установлю точку останова в конструкторе Foo (после удаления dll бара), она не будет достигнута. Это означает, что если бы я установил в конструкторе оператор, который создает критический ресурс (до обновления Bar), он не был бы выполнен, поэтому нет необходимости вызывать финализатор:

// in class Foo
public Foo() {
    // ...
    other = new OtherResource(); // this is not called when Bar's dll is missing
    external = new Bar();        // runtime throws before entering the constructor
}

protected virtual void Dispose(bool disposing) {
    // ...
    other.Dispose();    // doesn't get called either, since I am
    external.Dispose(); // invoking a method on external
    // ...
}

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

public abstract class DisposableBase : IDisposable {
    private readonly bool constructed;
    protected DisposableBase() {
        constructed = true;
    }
    ~DisposableBase() {
        if(!constructed) return;
        this.Dispose(false);
    } 
    /* ... */
}   

Ответы [ 5 ]

9 голосов
/ 06 октября 2011

Почему вызывается финализатор, хотя экземпляр класса не создан?

Вопрос не имеет смысла.Очевидно, экземпляр создан;что завершит финализатор, если не будет создан экземпляр?Вы пытаетесь сказать нам, что в этом финализаторе нет ссылки "this"?

конструктор не был вызван

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

Вы, кажется, думаете, что только из-за того, что конструктор не может быть вызван, экземпляр не может быть создан.Это не следует логически вообще.Ясно, что должен быть экземпляр до , когда вызывается ctor, потому что ссылка на этот экземпляр передается ему как "this".Таким образом, менеджер памяти создает экземпляр - и сборщик мусора знает, что выделена память - и затем вызывает конструктор.Если вызов конструктора вызывает исключение - или прерывается асинхронным исключением, таким как прерывание потока - там все еще есть экземпляр, известный сборщику мусора, и, следовательно, нуждающийся в финализации, когда он не работает.

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

финализатор все еще вызывается (в потоке GC), что в конечном итоге приводит к сбою приложения.

Затем исправьте финализатор так, чтобы он этого не делал.

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

Если я установлю точку останова в конструкторе Foo (после удаления dll бара), он не попадет.

Правильно.Как я уже сказал, тело конструктора не может быть соединено.Как вы могли бы достичь точки останова в методе, который даже не может быть объединен?

Это означает, что если бы я установил оператор в конструкторе, который создает критический ресурс (до обновления Bar), он бы нене будет выполнено, следовательно, нет необходимости вызывать финализатор.

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

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

2 голосов
/ 25 сентября 2011

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

Это совершенно законное поведение. Предположим, что класс частично построен, и открыл какой-то критический ресурс, прежде чем он бросил? Что будет, если финализатор не запустится? Примеры для этого упрощены в C #, но в C ++ это было предметом многих постов и книг (Sutter: Exceptional C ++).

SO: вызывается финализатор, если конструктор выдает (C ++ / c #)

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

Снимок экрана: ошибка привязки внутри потока финализатора. Если вы закомментируете строку удаления для Bar, исключение исчезнет, ​​а ошибка привязки - нет.

enter image description here

1 голос
/ 05 октября 2011

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

.NET Framework действительно предполагает, что сборки, на которые вы ссылаетесь, и типы, которые вы используете, присутствуют во время выполнения. Если вы хотите более динамичную систему, архитектуру плагинного типа, вам нужно по-разному проектировать свою сборку и типы, например, используя такие вещи, как пространство имен System.Addin или другие библиотеки, такие как MEF (см. Это на SO: между MEF и MAF (System.AddIn) )

Итак, в вашем случае вы можете решить проблему в Foo следующим образом:

public class Foo : IDisposable
{
    // use another specific interface here, like some IBar,
    // this is a sample, so I use IDisposable which I know is implemented by Bar
    private readonly IDisposable external;
    public Foo()
    {
        Console.WriteLine("Foo");
        external = Activator.CreateInstance(Type.GetType("AssemblyB.Bar, AssemblyB")) as IDisposable;
    }

    ... same code    
}

Но это также означает рефакторинг ...

1 голос
/ 04 октября 2011

Это предположение, но я попробовал что-то вроде этого:

public class Foo : IDisposable {
private Bar external;
private bool disposed;

public static Foo CreateFoo() {
    Foo foo = new Foo();
    foo.external = new Bar();
    return foo;
}

private Foo() {
}

~Foo() {
    Console.WriteLine("~Foo");
    this.Dispose(false);
}
public void Dispose() {
    this.Dispose(true);
    GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
    if (disposed) return;
    if (disposing) external.Dispose();
    disposed = true;
}

}

Этот образец будет работать так, как вы ожидаете.

А теперь мое предположение:

Я думаю, что конструктор не первый метод, который вызывается, когда объект будет создан. Похоже, что-то вне нашего контроля выделяет пространство для созданного объекта до вызова конструктора, или что-то подобное. Это тот момент, когда я предполагаю, что GC начнет работать. Он получает ClassInfo и знает, что финализатор доступен. Таким образом, он запускает Finalizer-Thread и создает дескриптор этой выделенной памяти. Знайте, что конструктор будет вызван, и объект будет создан из этого дескриптора. Но перед вызовом конструктора (даже метода) что-то проверяет наличие всех ссылочных типов в этом блоке кода. Это точка, где выдается исключение FileNotFoundException. Обратите внимание, что прежде чем вы увидите, как отладчик входит в конструктор. Теперь мы переходим в Finalizer-Thread, он смотрит на свой дескриптор Foo и видит, что этот дескриптор больше не используется (на самом деле он никогда не использовался). Начинается финализация. Это точка, где называется ваш финализатор. Вы получаете доступ к методу Dispose (bool) внутри, и в этот момент будет выброшено второе исключение FileNotFoundException, поскольку вы обращаетесь к классу Bar в этом методе. Это исключение сгенерирует эту непонятную проверку метода перед вызовом. Я предполагаю, что это как-то связано с некоторыми оптимизациями, может быть, ссылочные типы загружаются лениво.

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

Edit:

Лучшее решение было бы довольно простым. Удалите финализатор из вашего класса. У вас не используются неуправляемые ресурсы, поэтому вам вообще не понадобится финализатор или Dispose (bool).

1 голос
/ 24 сентября 2011

Возможно, вы захотите поместить свой класс Foo в сборку третьей сборки C, а затем сначала настроить обработчик для события AppDomain.CurrentDomain.AssemblyResolve в основной функции.Затем попытайтесь загрузить и выполнить класс Foo через Reflection;таким образом, вы можете проверить, существует ли файл и правильно ли реагировать.После этого обработчик события будет запускаться всякий раз, когда отсутствует сборка (начиная с прямых зависимостей AssemblyC, в данном примере это будет класс Bar).

...