Ответ 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
, что будет оптимизацией, которая все равно приведет к правильному поведению. Если бы это произошло, то ваши два метода стали бы намного более похожими в базовом поведении и, следовательно, с большей вероятностью были бы похожими на наблюдаемое поведение.