понедельник, 19 августа 2013 г.

Слабые ссылки в .NET

Никогда не задумывались о том, что значит булевый флаг trackResuraction в конструкторе WeakReference и что же такое short weak reference и long weak reference? Есть шансы, что вы не задумывались об этом просто потому, что никогда этих зверей не использовали в своем коде. Я тоже не особо задумывался об этой разнице, но вот, давеча, решил разобраться с этими вещами более подробно.

ПРИМЕЧАНИЕ
Разница между short и long weak references существует лишь при работе с финализируемыми объектами. Выходит, что это весьма специфическая тема, но ее рассмотрение позволит чуть глубже разобраться с внутренним устройством и поведением сборщика мусора.

Итак, давайте вспомним, что происходит при создании объекта А, содержащего финализатор:

  1. Выделяется память в управляемой куче.
  2. Если объект реализует финализатор (в нашем случае это так), то указатель на него кладется в очередь для финализации (finalization queue).
  3. Вызывается конструктор объекта А.

После чего ссылка на вновь созданный объект сохраняется в локальной переменной или поле объекта:

image

Предположим, что в некоторый момент времени на вновь созданный объект больше не остается ссылок из корней приложения (application roots); при этом ссылка из finalization queue не рассматривается в качестве корневой. Тогда во время ближайшей сборки мусора объект считается достижимым для сборки, но прежде чем расправиться с ним окончательно, GC все таки проверяет наличие ссылки на него в finalization queue.

Поскольку ссылка на объект А находится в finalization queue, то вместо удаления объекта ссылка на него сохраняется в другой очереди: в очереди объектов, готовых для финализации (freachable queue). Поскольку объект выживает после первой сборки мусора, поэтому он переходит в первое поколение объектов.

image

В отличие от finalization queue, ссылка из freachable queue рассматривается сборщиком мусора в качестве корневой. Поскольку до сборки мусора корневых ссылок на объект не было, а после копирования ссылки на объект в freachable queue она появляется, говорят, что в этот момент объект воскресает.

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

А при чем здесь слабые ссылки?

Как мы увидели, финализируемый объект умирает дважды. Первый раз, когда на него не остается ссылок из корней приложения, и второй раз – после следующей сборки мусора, уже после вызова финализатора этого объекта. Так когда, в таком случае, свойство Target слабой ссылки должно обнуляться: во время первого этапа сборки мусора или во время второго?

Теперь должно быть не сложно догадаться, для чего нужен флаг trackResurection у конструктора WeakReference. По умолчанию (trackResurection – false), слабая ссылка является короткой (short weak reference), при этом Target этой ссылки сбрасывается в null во время первого этапа сборки мусора, т.е. когда на объект пропадают ссылки из кода приложения. А для длинной слабой ссылки (long weak reference) – во время второго!

Вот небольшой пример, который демонстрирует разницу в поведении:

// Кастомный объект с финализатором
class Finalizable
{
    ~Finalizable()
    {
       
Console.WriteLine("Finalizable.dtor"
);
    }
}

// Простой трекер, который генерирует событие, когда объект
// на который указывает слабая ссылка умирает

class WeakReferenceTracker
{
   
private readonly WeakReference
_wr;

   
public WeakReferenceTracker(object o, bool
trackResurection)
    {
        _wr =
new WeakReference
(o, trackResurection);

       
// Начинаем следить за тем, когда объект умрет!
        Task
.Factory.StartNew(TrackDeath);
    }

   
public event Action
ReferenceDied = () => { };

   
// Не слишком надежная реализация, но для наших целей вполне подходящая
    private void
TrackDeath()
    {
       
while (true
)
        {
           
if
(!_wr.IsAlive)
            {
                ReferenceDied();
               
break
;
            }
           
Thread.Sleep(1);
        }
    }
}

И теперь достаточно создать два разных трекера для одного финализируемого объекта и посмотреть порядок обнуления слабых ссылок:

public static void Main()
{
   
Console.WriteLine("Creating 2 trackers..."
);

   
var finalizable = new Finalizable
();

   
var weakTracker = new WeakReferenceTracker(finalizable, false
);
    weakTracker.ReferenceDied +=
        () =>
Console.WriteLine("Short weak reference is dead"
);

   
var resurectionTracker = new WeakReferenceTracker(finalizable, true
);
    resurectionTracker.ReferenceDied +=
        () =>
Console.WriteLine("Long weak reference is dead"
);

   
Console.WriteLine("Forcing 0th generation GC..."
);
   
GC
.Collect(0);
   
Thread
.Sleep(100);

   
Console.WriteLine("Forcing 1th generation GC..."
);
   
GC
.Collect(1);

   
// Это предотвратит уничтожение сборщиком мусора самих трекеров
    GC
.KeepAlive(weakTracker);
   
GC
.KeepAlive(resurectionTracker);

   
Console.ReadLine();
}

Запускаем это чудо без отладки и получаем следующий вывод:

Creating 2 trackers...
Forcing 0th generation GC...
Finalizable.dtor
Short weak reference is dead
Forcing 1th generation GC...
Long weak reference is dead

ПРИМЕЧАНИЕ
Мы видим, что вначале вызывает финализатор нашего объекта и лишь потом "обнуляется" короткая слабая ссылка. Дело в том, что у потока финализатора очень высокий приоритет, поэтому когда мой велосипед замечает, что слабая ссылка обнулена, финализатор уже успевает отработать.

Исходя из этого вывода на экран мы видим, что short weak reference умирает после того, как объект стал недостижим из кода приложения, а long weak reference умирает лишь после окончательного удаления объекта во время следующей сборки мусора.

Дополнительные статьи о сборке мусора

11 комментариев:

  1. А как поведёт себя long weak reference, есть в финализаторе сохранить ссылку на объект куда-нибудь (т.е. вновь создать app root)?

    ОтветитьУдалить
  2. @Pavel: поскольку в этом случае объект воскреснет окончательно, то длинная слабая ссылка продолжить указывать на воскресший объект.

    ОтветитьУдалить
  3. Почему то у меня код не заработал, объекты не хотели умирать) сделал вот так
    _wr = new WeakReference(new Finalizable(), trackResurection);
    и протестировал для каждого кейса отдельно.
    Но тут также нужно добавить, что помимо того что он окончательно воскреснет, ссылки на него в финализейшин кю больше нет и как показали мои наивные тесты в фричебел кю тоже(так как финализатор больше не вызывался) значит автоматом после того как вызвалась финализация для объекта, он автоматически удалиться из фричебел кю.

    П.С. достаточно интересный кейс получается, как раз для собеседований )

    ОтветитьУдалить
  4. Игорь, а вы пробовали в релизной сборке и запускали по Ctrl + F5? Тогда должно работать.

    Да, конечно, объект из очередей удалится автоматом. А если вы его воскресите полность (сохраните ссылку на this во время вызова финализатора), то объект снова можно поместитьв очередь финализации путем вызова GC.ReRegisterForFinalize.

    ОтветитьУдалить
  5. Спс, действительно забыл про Ctrl + F5.

    ОтветитьУдалить
  6. >>Мы видим, что вначале вызывает >>финализатор нашего объекта и лишь >>потом "обнуляется" короткая >>слабая ссылка.

    Несколько конфюзит эта фраза. В теории выше ты пишешь, что короткая ссылка умирает, когда объект умирает в первый раз, т.е. когда AppRoots не указывают на объект. Заметь, в этот момент объект ТОЛЬКО перемещается во freachable. Финализатор будет вызван только на след. шаге сборки мусора. Я увидел твой мессадж, что нить финализаторов выполняется очень приоритетно, однако это все равно не объясняет вывода на консоль, где у тебя сначала срабатывает финализатор и лишь потом умирает "короткая" ссылка. Можешь поподробнее здесь?

    ОтветитьУдалить
  7. @eugene: финализатор вызывается не после второй сборки мусора, а сразу после первой сборки (асинхронно).

    Порядок сборки мусора такой:
    1. Теряются все корни приложения.
    2. Финализируемый объект перемещается из finalization queue в freachable queue (и воскресает)
    3. Асинхронно вызывается финализатор в новом потоке
    4. Ссылка удаляется из freachable queue и объект становится достижим для сборки (умирает второй раз)
    5. Во время следующей сборки он полностью удаляется.

    Чтобы убедиться в этом, достаточно вызвать лишь один раз GC.Collect и увидеть, что сразу же после этого вызовется финализатор.

    Фишка же вся в том, что успешное завершение финализатора является предусловием для недостижимости объекта. До тех пор пока он не отработает, объект является живым (точнее воскресшим).

    Ведь именно поэтому для полной очистки объектом есть такой паттерн:

    GC.Collect();
    // По завршению след. вызова все финализируемые ("воскрешсие") объекты
    // вновь становятся достижимыми для сборки
    GC.WaitForPendingFinalizers();
    // Добиваем их окончательно
    GC.Collect();

    ОтветитьУдалить
  8. Это понятно. Это согласуется с моей картиной мира :). Я к словам цепляюсь. Когда увидел, что под WeakReference лежит- это многое объяснило. Тогда новый раунд :():
    Цитирую -
    Как мы увидели, финализируемый объект умирает дважды. Первый раз, когда на него не остается ссылок из корней приложения, и второй раз – после следующей сборки мусора, уже после вызова финализатора этого объекта.

    Написано правильно, спору нет. Но вывод на консоль у тебя другой. Получается, что у тебя в примере короткая ссылка умерла после финализатора, а это не так. Т.е по хорошему надо было бы как-то поймать IsAlive до финализатора, но после 1 сборки. В этом случае короткая ссылка должна быть мертвой.
    П.Ы. А где это может потребоваться? Мне что-то в голову ничего не идет....

    ОтветитьУдалить
  9. Ок, продолжим:)

    Чтобы получить правильный вывод достаточно в финализаторе поставить Thread.Sleep(100), и в этом случае мы увидим, что слабая ссылка обнуляется еще до завершения финализатора.

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

    П.Ы. Может понадобиться для трекания полной смерти тасок, например, или любого другого финализируемого объекта, финализатор которого что-то делает.

    П.Ы.Ы. В реальности это означает: никогда, или практически никогда:)

    ОтветитьУдалить
  10. Сходимость сошлась :)!!!
    Да, так ок.

    ОтветитьУдалить