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

Методы асинхронного сбора мусора

Я только что заметил что-то действительно странное в отношении сбора мусора.

Метод WeakRef собирает объект, как ожидалось, в то время как метод async сообщает, что объект все еще жив, хотя мы вынудили сборку мусора. Любые идеи, почему?

class Program
{
    static void Main(string[] args)
    {
        WeakRef();
        WeakRefAsync().Wait();
    }

    private static void WeakRef()
    {
        var foo = new Foo();
        WeakReference fooRef = new WeakReference(foo);
        foo = null;
        GC.Collect();
        Debug.Assert(!fooRef.IsAlive);
    }

    private static async Task WeakRefAsync()
    {
        var foo = new Foo();
        WeakReference fooRef = new WeakReference(foo);
        foo = null;
        GC.Collect();
        Debug.Assert(!fooRef.IsAlive);
    }
}


public class Foo
{

}
4b9b3361

Ответ 1

Метод WeakRef собирает объект как ожидалось

Нет причин ожидать этого. Например, в Linqpad это не происходит в сборке отладки, хотя другие допустимые компиляции как отладочных, так и релизных сборок могут иметь либо поведение.

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

Итак, так как оба с и без foo = null для GC имеют значение, чтобы видеть foo как корневое или не внедренное, мы можем разумно ожидать либо поведения.

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

Хорошо, давайте посмотрим, что на самом деле происходит здесь.

Машина состояний, созданная методом async, представляет собой структуру с полями, соответствующими локальным объектам в источнике.

Итак, код:

var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
GC.Collect();

Немного напоминает:

this.foo = new Foo();
this.fooRef = new WeakReference(foo);
this.foo = null;
GC.Collect();

Но для доступа к полю всегда есть что-то, происходящее локально. Поэтому в этом отношении почти, например:

var temp0 = new Foo();
this.foo = temp0;
var temp1 = new WeakReference(foo);
this.fooRef = temp1;
var temp2 = null;
this.foo = temp2;
GC.Collect();

И temp0 не был обнулен, поэтому GC находит foo как корневое.

Два интересных варианта вашего кода:

var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
await Task.Delay(0);
GC.Collect();

и

var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
await Task.Delay(1);
GC.Collect();

Когда я его запускал (опять же, разумные различия в том, как обрабатываются память/регистры для локальных жителей, могут приводить к различным результатам), первый имеет то же поведение вашего примера, потому что, когда он вызывает другой метод Task и await он, этот метод возвращает завершенную задачу, так что await немедленно перемещается на следующую вещь внутри одного и того же метода вызова метода, который является GC.Collect().

Второе имеет поведение, когда мы собрали foo, потому что в этой точке возвращается await, а затем машина состояния имеет свой метод MoveNext(), вызываемый снова примерно миллисекундой позже. Поскольку это новый вызов метода "за кадром", локальная ссылка на foo отсутствует, поэтому GC действительно может его собрать.

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