IDisposable + шаблон финализатора

Глядя на шаблон IDisposable + шаблон Finalizer, я кое-что не понимаю:

public class ComplexCleanupBase : IDisposable
{
    private bool disposed = false; // to detect redundant calls

    public ComplexCleanupBase()
    {
        // allocate resources
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // dispose-only, i.e. non-finalizable logic
            }

            // shared cleanup logic
            disposed = true;
        }
    }

    ~ComplexCleanupBase()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Насколько я понимаю, шаблон должен быть реализован, как указано выше.

1) Вызов Dispose () запускает GC.SuppressFinalize (this), что означает, что объект не следует помещать в очередь финализатора, поскольку он уже правильно удален? Это помогает быстрее освободить объект?

2) Но что, если я вообще не вызываю Dispose () для этого объекта? В этом случае должен сработать финализатор, верно? Но Dispose (ложь); абсолютно ничего не делает (только установка disposed = true). Это предназначено? Такое ощущение, что чего-то не хватает ...


person Houman    schedule 03.01.2012    source источник


Ответы (2)


Вопрос 1: Да, если GC.SuppressFinalize не вызывается, объект будет помещен в очередь финализатора И переместится вверх на поколение (если еще не на поколение 2), что означает, что память для этого объекта не будет освобождена до следующего проход GC для нового поколения.

Вопрос 2: Ваш комментарий //shared cleanup logic - это то, куда пойдет общая логика очистки, здесь могут произойти другие вещи, кроме настройки disposed = true.

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

public class ComplexCleanupBase : IDisposable
{
  private volatile bool disposed = false; // to detect redundant calls
  private readonly object _mutex = new object();

  protected virtual void Dispose(bool disposing)
  {
    if (!Monitor.TryEnter(_mutex))
    {
      // We're already being disposed.
      return;
    }

    try
    {
      if (!disposed)
      {
        if (disposing)
        {
            // dispose-only, i.e. non-finalizable logic
        }

        // shared cleanup logic
        disposed = true;
      }
    }
    finally
    {
      Monitor.Exit(_mutex);
    }
  }
  ... other methods unchanged
}
person Rich O'Kelly    schedule 03.01.2012
comment
Финализаторы не должны блокировать получение блокировки, даже если предполагается, что блокировки не будут удерживаться очень долго. Если что-то вызывает неправильное удержание блокировки, это может помешать запуску любых других финализаторов. - person supercat; 20.01.2014
comment
@supercat В этом случае невозможно заблокировать вызов в финализаторе. _mutex следует использовать только для утилизации. В целом вы правы, однако в этом случае ваша точка зрения (должна быть) спорна. - person Rich O'Kelly; 20.01.2014
comment
@supercat Просто подумал о ситуации, которая делает мой предыдущий комментарий некорректным: если во время завершения другого экземпляра он возвращает к жизни экземпляр ComplexCleanupBase, так что у него есть другой корень в приложении, возможно возникновение конфликта за мьютекс, хотя это не должно действительно быть сделано. Ответ все таки обновил. - person Rich O'Kelly; 20.01.2014
comment
Использование TryEnter является правильным при вызове из финализатора; Dispose, однако, интересный случай. Часть контракта для Dispose подразумевает, что повторный вызов должен быть безопасным; другой аспект определяет, что ресурсы должны быть доступны для немедленного повторного использования, как только Dispose вернется. Мне действительно хотелось бы, чтобы метод Dispose мог условно генерировать исключение в зависимости от того, было ли оно вызвано в результате основного кода или очистки исключения в блоке _6 _ / _ 7_; в таком случае Dispose следует использовать TryEnter с большим таймаутом и ... - person supercat; 20.01.2014
comment
... генерировать исключение, если блокировке нужно ждать дольше, чем когда-либо было необходимо и Dispose был вызван из основного кода (а не из кода очистки исключения). Обратите внимание: если TryEnter не может войти немедленно, это будет означать, что после получения блокировки он просто снимет ее, но он будет знать, что код, который установил блокировку, завершил попытку очистки ресурса (должен быть установлен флаг. указать, было ли оно успешным; в случае неудачи последующие Dispose вызовы в идеале будут вызывать исключения только при вызове из основного случая). - person supercat; 20.01.2014
comment
Что касается воскрешения, то способ реализации финализации означает, что нет способа любого класса с финализатором, который может быть уверен, что финализатор не будет работать, пока существуют живые ссылки, поскольку для злой класс, который получил единственную существующую ссылку на объект, чтобы сохранить его в уже существующем финализуемом объекте, отказаться от всех других ссылок на любой из двух объектов, принудительно создать сборщик мусора и заставить злой финализируемый объект воскресить ссылку, а затем заблокировать нить финализатора на некоторое время. Таким образом, злой класс получил единственную ссылку на объект ... - person supercat; 20.01.2014
comment
... может обманом заставить сборщик мусора запланировать Finalize вызов этого объекта, в то время как другой код считает, что объект жив, и все еще использует его. Жаль, что нет никакого способа сказать, что длинная слабая ссылка продолжает воскрешать себя, пока существует цель, поскольку это упростит защиту от некоторых сценариев воскрешения. - person supercat; 20.01.2014

Если Dispose (false) ничего не сделает, это очень хороший признак того, что ни ваш класс, ни какой-либо производный от него класс не должен включать «деструктор» в стиле C # или переопределять Finalize, и следует учитывать аргумент «удаление». как пустышка, цель которой - дать защищенному методу Dispose отличную от общедоступной сигнатуру.

Обратите внимание, что реализация деструктора или переопределение Finalize в производном классе, когда родительский класс не ожидает такого поведения, может вызвать Heisenbugs. Помимо прочего, сборщик мусора иногда может решить, что объект класса был оставлен, вызывая его финализатор / деструктор, даже когда используется объект, на который ссылается поле класса. Например, предположим, что статический класс usbThingie управляет контроллерами USB, используя целочисленные дескрипторы, а класс-оболочка usbWrapper делает что-то вроде:

  UInt32 myHandle;
  void sendData(Byte data[])
  {
    UsbThingie.send(myHandle, data[0], data.Length);
  }

Если вызов sendData () - это последнее, что делается с экземпляром usbWrapper перед тем, как он будет оставлен, сборщик мусора может заметить, что после вызова UsbThingie.send () - даже до того, как он вернется - нет дальнейшие ссылки будут существовать на usbWrapper, и поэтому он может безопасно запускать финализатор. Если финализатор попытается закрыть канал, указанный в myHandle, это может прервать передачу, которая имела место; если usbThingie не является потокобезопасным, неизвестно, что может случиться.

person supercat    schedule 03.01.2012