воскресенье, 9 января 2011 г.

Finalizer, Как работает GC?

В .Net GC реализует алгоритм "Пометить, Освободить, Упаковать" описанный например здесь. Изначально GC рассматривает все объекты как недостижимые. В процессе прохода по графу он помечает все достижимые объекты флажком, а все оставшиеся, соотвтетственно, подлежат сборке. Такой алгоритм осуществляется в один проход и позволяет легко определять и уничтожать "острова изоляции", а также не подвержен проблеме зацикленных ссылок.
Значительный интерес представляет то, как GC поступает с объектами, имеющими файналайзеры. Можно ли из файналайзера обращаться к другим reference type объектам? Могут ли эти reference type объекты оказаться уже уничтоженными и что тогда произойдет?
Давайте определим термины:
завершенными мы будем называть объекты для которых были вызваны их файналайзеры.
уничтоженными или собранными мы будем называть те объекты, память занимаемая которыми была освобождена и возвращена куче.
Сразу же скажу, что, в отличии от C++, обратиться к уничтоженным объектам в .Net н е в о з м о ж н о. Зато обращение к завершенным объектам вполне допустимо, более того, они даже могут быть "воскрешены".

На нижеприведенной диаграмме приблизительно описан жизненный цикл reference type объектов:



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

Условные обозначения на диаграмме:
Finalization Queue - очередь финализации (терминология из MSDN)
Freachable Queue - согласно терминологии MSDN, это список объектов готовых к завершению (финализации). Объекты попавшие в этот список, а так же все объекты на которые они ссылаются и так далее, не подлежат уничтожению и переживают сборку мусора. Поэтому из файналайзера можно смело обращаться к любым объектам, все они живы, другое дело что они могут оказаться уже завершенными.
Рассмотрим следующую ситуацию:

class ClassA
{
  bool _disposed;

  public void DoSomething()
  {
    if (_disposed)
    throw new ObjectDisposedException("ClassA");
  }

  ~ClassA()
  {
    _disposed = true;
  }
} //class


class ClassB
{
  ClassA _classA;

  public ClassB()
  {
    _classA = new ClassA();
  }

  ~ClassB()
  {
    _classA.DoSomething();
  }
} //class

* This source code was highlighted with Source Code Highlighter.

К моменту когда ClassB в файналайзере обращается к ClassA.DoSomething экземпляр ClassA может оказаться уже завершенным и в этом случае метод DoSomething кинет ObjectDisposedException. Если исключение корректно обработается в вызывающем коде, то никаких проблем нет. Если же оно "пролетит мимо" то это может привести к аварийному завершению всего приложения согласно тексту из MSDN. Вот почему в файналазерах необходимо осторожно работать с обращениями к другим reference type объектам.

Теперь об оживлении.
В файналайзере объект имеет право передать другому, в том числе и живому объекту ссылку на самого себя. В этом случае он и все объекты на которые он ссылается вновь окажутся доступными для кода программы и как бы воскреснут. Правда восрешенный объект не будет больше состоять в Finalization Queue и когда он следующий раз окажется недоступным, его файналайзер больше не будет вызван и объект окажется просто уничтоженным. Для того, чтоб вновь поставить объект в очередь финализации нужно воспользоваться вызовом
GC.ReRegisterForFinalize
Хотя лично мне подобный трюк не нравится потому, что он, например, может привести к появлению бессмертных Дунканов Маклаудов, утечкам памяти и излижним нагрузкам на GC.

Ниже приведен пример кода в котором объект восстанавливается в файналайзере:

class RecoverableObject
{
  int _id;
  static int Counter;
  bool _canDie;

  public static RecoverableObject Root;

  static RecoverableObject()
  {
    Root = new RecoverableObject();
    Root.CanDie = true;
  }

  public RecoverableObject()
  {
    _id = ++Counter;
  }

  ~RecoverableObject()
  {
    if (CanDie)
    {
      Console.WriteLine("good bye Id={0}", _id);
      return;
    }

    Console.WriteLine("obj.Id={0} is going to be recovered", _id);
    Root.Other = this;
    GC.ReRegisterForFinalize(this);
  }

  public int Id
  {
    get
    {
      return _id;
    }
  }

  public bool CanDie
  {
    get
    {
      return _canDie || this == Root;
    }
    set
    {
      _canDie = value;
    }
  }

  public RecoverableObject Other { get; set; }



  static void RecoverableObjectTest()
  {
    RecoverableObject obj;

    obj = new RecoverableObject();
    Console.WriteLine("first collection...");
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Console.WriteLine("after the first collection");
    Console.WriteLine();

    obj = RecoverableObject.Root.Other;
    RecoverableObject.Root.Other = null;
    Console.WriteLine("obj.Id={0} is available again", obj.Id);
    Console.WriteLine("second collection...");
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Console.WriteLine("after the second collection");
    Console.WriteLine();

    obj = RecoverableObject.Root.Other;
    RecoverableObject.Root.Other = null;
    Console.WriteLine("obj.Id={0} is available again", obj.Id);
    obj.CanDie = true;
    Console.WriteLine("third collection, obj.Id={0} must die!", obj.Id);
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Console.WriteLine("after the third collection");
    Console.WriteLine();

    Console.WriteLine("RecoverableObjectTest exiting...");
  }

} //class



* This source code was highlighted with Source Code Highlighter.

Вкратце, вот что нужно знать о финализации:
  1. Все объекты, имеющие метод Finalize помещаются в очередь финализации при их создании. Уничтожение объектов помещенных в очередь финализации производится как минимум в два этапа (может быть и больше, если их оживлять)
  2. Метод Finalize не должен выдавать исключения, поскольку они не могут обрабатываться приложением и могут привести к завершению работы приложения.
  3. Реализация методов Finalize (деструкторов) может отрицательно воздействовать на производительность, и злоупотреблять ими не рекомендуется.


Данный пост отвечает на вопрос, поставленный в посте "Qyoto, Issues for investigation" о том правомерно ли обращаться из файналайзера к reference type объектам. Отвечает утвердительно: "Да, правомерно"

В ближайших статьях я постараюсь дать ответы на оставшиеся вопросы.

четверг, 6 января 2011 г.

Qyoto, issues for investigation

Я выкачал исходники Qyoto и нашел в них несколько моментов, требующих более глубокого изученения:
  1. P/Invoke. Все managed обертки над Qt-шными классами содержат в себе член SmokeInvokation, через который осуществляются вызовы unmanaged кода и вызовы managed callback функций. Внутри этого класса все вызовы осуществляются через P/Invoke. Насколько это снизит производительность приложения? Предварительные тесты (на .Net Framework 4.0 показали), что вызов unmanaged функции без параметров через P/Invoke при отстутствии нагрузки на GC осуществляются в 50-70 раз медленнее, чем вызов аналогичной managed функции. Если в среднем managed код исполняется в 2-3 раза медленнее чем unmanaged и при этом он будет еще и осуществлять многократные вызовы через P/Invoke, то такой код будет работать в (2..3)*(50..70) = (100..200) раз медленнее чем unmanaged. Для нехитрого GUI со стандартными контролами это все равно кажется некритичным, но если вы, например, собираетесь отображать геоинформационные данные и ваши требования к производительности GUI высоки, то такое замедление видится неприемлемым. Более того, оно будет даже более существенным в случае маршаллинга данных и высоких нагрузок на GC. Тем не менее нужно изучить каковы накладные расходы на P/Invoke в Mono, может статься, что они не так уж велики.
  2. Следующий момент, это вызов деструкторов Qt-шных объектах в файналайзерах их managed оберток. Во-первых файналайзеры создают дополнительную нагрузку на GC и снижают производительность. Во-вторых в классах, поддерживающих интерфейс IDisposable, в методах Dispose() тоже вызываются деструкторы unmanaged объектов, но сами обертки при этом не помечаются как не нуждающиеся в финализации вызовом GC.SuppressFinalize(). В третьих, обращение к reference type объектам (SmokeInvokation) из файналайзеров кажется мне непрвильным, потому, что порядок уничножения reference type объектов недетерменирован и, таким образом, SmokeInvokation объект к моменту обращения уже может оказаться собранным GC. Это тоже нужно проверить.
  3. И наконец, если ref type объект был создан в некоторой функции и начиная с некоторого момента в создавшей его функции больше не будет обращений к нему, то GC разрешено собрать его, не дожидаясь выхода потока управления из данной, создавшей объект функции (я добавлю здесь пояснения). Таким образом может получится, что файналайзер объекта будет вызван из потока GC в то время пока unmanaged код все еще будет работать с его unmanged ресурасами. Чтоб предотвратить такую ситуацию нужно использовать HandleRef или GCHandle совместно с GC.KeepAlive. Нужно также проверить, как в Qyoto разруливается описанная ситуация и предусмотренна ли она вообще.