Объект C# WeakReference имеет значение NULL в финализаторе, хотя на него все еще ссылаются

Привет, у меня есть код, в котором я не понимаю, почему я попал в точку останова (см. комментарий).

Является ли это ошибкой Microsoft чего-то, чего я не знаю или неправильно понимаю?

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

Примечание. Вы можете протестировать код непосредственно в консольном приложении.

ПРОСТО ДЛЯ ИНФОРМАЦИИ... следуя ответу supercat, я исправил свой код с предложенным решением, и он прекрасно работает :-) !!! Плохо то, что используется статический диктофон, и производительность идет вместе с ним, но это работает. ... Через несколько минут я понял, что SuperCat дает мне все подсказки, чтобы сделать это лучше, чтобы обойти статический словарь, и я это сделал. Примеры кода:

  1. Код с ошибкой
  2. Код исправлен, но со статической ConditionalWeakTable
  3. Код с ConditioalWeakTable, включающий приемы SuperCat (большое спасибо ему!)

Образцы...

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace WeakrefBug
{

// **********************************************************************
class B : IDisposable
{
    public static List<B> AllBs = new List<B>();

    public B()
    {
        AllBs.Add(this);
    }

    private bool disposed = false;
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            AllBs.Remove(this);
            disposed = true;
        }
    }

    ~B() { Dispose(false); }
}

// **********************************************************************
class A
{
    WeakReference _weakB = new WeakReference(new B());

    ~A()
    {
        B b = _weakB.Target as B;
        if (b == null)
        {
            if (B.AllBs.Count == 1)
            {
                Debugger.Break(); // b Is still referenced but my weak reference can't find it, why ?
            }
        }
        else { b.Dispose(); }
    }
}

// **********************************************************************
class Program
{
    static void Main(string[] args)
    {
        A a = new A();
        a = null;

        GC.Collect(GC.MaxGeneration);
        GC.WaitForPendingFinalizers();
    }
    }

    // **********************************************************************
}

Версия исправлена:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace WeakrefBug // Working fine with ConditionalWeakTable
{
    // **********************************************************************
    class B : IDisposable
    {
        public static List<B> AllBs = new List<B>();

        public B()
        {
            AllBs.Add(this);
        }

        private bool disposed = false;
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                AllBs.Remove(this);
                disposed = true;
            }
        }

        ~B() { Dispose(false); }
    }

    // **********************************************************************
    class A
    {
        private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<A, B> WeakBs = new ConditionalWeakTable<A, B>();

        public A()
        {
            WeakBs.Add(this, new B());          
        }

        public B CreateNewB()
        {
            B b = new B();
            WeakBs.Remove(this);
            WeakBs.Add(this, b);
            return b;
        }

        ~A()
        {
            B b;
            WeakBs.TryGetValue(this, out b);

            if (b == null)
            {
                if (B.AllBs.Count == 1)
                {
                    Debugger.Break(); // B Is still referenced but my weak reference can't find it, why ?
                }
            }
            else { b.Dispose(); }
        }
    }

    // **********************************************************************
    class Program
    {
        static void Main(string[] args)
        {
            A a = new A();
            WeakReference weakB = new WeakReference(a.CreateNewB()); // Usually don't need the internal value, but only to ensure proper functionnality
            a = null;

            GC.Collect(GC.MaxGeneration);
            GC.WaitForPendingFinalizers();

            Debug.Assert(!weakB.IsAlive);
        }
    }

    // **********************************************************************
}

Код с ConditioalWeakTable, включающий приемы SuperCat (большое спасибо ему!)

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace WeakrefBug // Working fine with non static ConditionalWeakTable - auto cleanup
{
    // **********************************************************************
    class B : IDisposable
    {
        public static List<B> AllBs = new List<B>();

        public B()
        {
            AllBs.Add(this);
        }

        private bool disposed = false;
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                AllBs.Remove(this);
                disposed = true;
            }
        }

        ~B() { Dispose(false); }
    }

    // **********************************************************************
    class A
    {
        private ConditionalWeakTable<object, object> _weakBs = null;

        public A()
        {
        }

        public B CreateNewB()
        {
            B b = new B();
            if (_weakBs == null)
            {
                _weakBs = new ConditionalWeakTable<object, object>();
                _weakBs.Add(b, _weakBs);
            }
            _weakBs.Remove(this);
            _weakBs.Add(this, b);
            return b;
        }

        internal ConditionalWeakTable<object, object> ConditionalWeakTable // TestOnly
        {
            get { return _weakBs; }
        }

        ~A()
        {
            object objB;
            _weakBs.TryGetValue(this, out objB);

            if (objB == null)
            {
                if (B.AllBs.Count == 1)
                {
                    Debugger.Break(); // B Is still referenced but my weak reference can't find it, why ?
                }
            }
            else
            {
                ((B)objB).Dispose();
            }
        }
    }

    // **********************************************************************
    class Program
    {
        static void Main(string[] args)
        {
            A a = new A();
            WeakReference weakB = new WeakReference(a.CreateNewB()); // Usually don't need the internal value, but only to ensure proper functionnality
            WeakReference weakConditionalWeakTable = new WeakReference(a.ConditionalWeakTable);
            a = null;

            GC.Collect(GC.MaxGeneration);
            GC.WaitForPendingFinalizers();

            Debug.Assert(!weakB.IsAlive);
            Debug.Assert(!weakConditionalWeakTable.IsAlive);
        }
    }

    // **********************************************************************

}

Следующий вопрос CitizenInsane... Я точно не помню, почему я сделал то, что сделал... Я нашел свой образец, но в то время не был уверен в своих намерениях. Я попытался понять это и пришел со следующим кодом, который, на мой взгляд, более ясен, но до сих пор не помню свою первоначальную потребность. Извини ???

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace WeakrefBug // Working fine with ConditionalWeakTable
{
    // **********************************************************************
    class B : IDisposable
    {
        public static List<B> AllBs = new List<B>();

        public B()
        {
            AllBs.Add(this);
        }

        private bool disposed = false;
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                AllBs.Remove(this);
                disposed = true;
            }
        }

        ~B() { Dispose(false); }
    }

    // **********************************************************************
    class A
    {
        private ConditionalWeakTable<object, object> _weakBs = null;
        private WeakReference _weakB = null;

        public A()
        {
            _weakBs = new ConditionalWeakTable<object, object>();
            B b = new B();
            _weakB = new WeakReference(b);
            _weakBs.Add(b, _weakB);
        }

        public B B
        {
            get
            {
                return _weakB.Target as B;
            }
            set { _weakB.Target = value; }
        }

        internal ConditionalWeakTable<object, object> ConditionalWeakTable // TestOnly
        {
            get { return _weakBs; }
        }

        ~A()
        {
            B objB = B;

            if (objB == null)
            {
                if (B.AllBs.Count == 1)
                {
                    Debugger.Break(); // B Is still referenced but my weak reference can't find it, why ?
                }
            }
            else
            {
                ((B)objB).Dispose();
            }
        }
    }

    // **********************************************************************
    class Program
    {
        static void Main(string[] args)
        {
            Test1();
            Test2();
        }

        private static void Test1()
        {
            A a = new A();
            WeakReference weakB = new WeakReference(a.B); // Usually don't need the internal value, but only to ensure proper functionnality
            WeakReference weakConditionalWeakTable = new WeakReference(a.ConditionalWeakTable);

            a = null;

            GC.Collect(GC.MaxGeneration);
            GC.WaitForPendingFinalizers();

            Debug.Assert(B.AllBs.Count == 0);

            GC.Collect(GC.MaxGeneration);
            GC.WaitForPendingFinalizers();

            Debug.Assert(!weakB.IsAlive); // Need  second pass of Collection to be collected
            Debug.Assert(!weakConditionalWeakTable.IsAlive);
        }

        private static void Test2()
        {
            A a = new A();
            WeakReference weakB = new WeakReference(a.B);

            B.AllBs.Clear();
            a.B = null;

            GC.Collect(GC.MaxGeneration);
            GC.WaitForPendingFinalizers();

            Debug.Assert(!weakB.IsAlive); // Need  second pass of Collection to be collected
        }
    }

    // **********************************************************************

}

person Eric Ouellet    schedule 26.02.2013    source источник
comment
Пожалуйста, уточните, это релиз или режим отладки. Кроме того, в комментарии говорится, что B все еще упоминается, но B является типом. Вы имели в виду b?   -  person Brian Rasmussen    schedule 26.02.2013
comment
Не может ли это быть потому, что при срабатывании точки останова b имеет значение null, поэтому GC все равно его собирает? Я не уверен.   -  person Kobunite    schedule 26.02.2013
comment
Все ставки сняты в финализаторе.   -  person leppie    schedule 26.02.2013
comment
@ Брайан Расмуссен: разъяснения сделаны, спасибо   -  person Eric Ouellet    schedule 26.02.2013
comment
@EricOuellet Спасибо. Отладка/выпуск важны в этом случае, потому что сборщик мусора в режиме выпуска может считать объекты подходящими для сбора, как только к ним больше не будет доступа в функции.   -  person Brian Rasmussen    schedule 26.02.2013
comment
@leppie: Я понимаю, что финализаторы особенные, написано ли где-то (ECMA) или где-то еще, что я не могу этого сделать. Это очень ограничительно, если я не могу? Если это недокументировано, это ошибка для меня. Но я хочу убедиться, прежде чем что-то сообщать.   -  person Eric Ouellet    schedule 26.02.2013
comment
Я бы согласился с тем, что говорит Брайан :)   -  person leppie    schedule 26.02.2013
comment
Другой вопрос: вы говорите, что считаете, что объект, на который указывает b, должен быть живым в точке Debugger.Break? Это было бы не так, поскольку вы попадете туда только в том случае, если b изначально было нулевым.   -  person Brian Rasmussen    schedule 26.02.2013
comment
@ Брайан, объекты не должны подходить для GC, если жесткая ссылка все еще существует, что имеет место здесь, как вы можете видеть в образце, где статическая коллекция содержит эту ссылку. Static - это корень для GC, и тогда мой объект никогда не собирался мусором... Я только теряю свой Weakref   -  person Eric Ouellet    schedule 26.02.2013
comment
ДОХ! Слишком много кода. Я даже не видел список до сих пор. Извини за это.   -  person Brian Rasmussen    schedule 26.02.2013
comment
@ Дэн, я действительно не уверен, что это только для неуправляемых ресурсов. Я почти уверен, что это также относится к очистке WeakEvent. Если у меня когда-нибудь будет свободное время, я напишу об этом статью в CodeProject. См. codeproject.com/Articles/29922/Weak-Events-in-C. от Даниэля Грюнвальда. Хотя это очень хорошая реализация, она пропускает слабые данные в обработчик в своем SmartWeakEvent, что может быть исправлено SmartHandler с финализатором, который сообщает, что это источник смерти. Моя ситуация похожа на эту.   -  person Eric Ouellet    schedule 27.02.2013
comment
@ Эрик, не могли бы вы пояснить, что такое трюк SuperCat? Я понял, что версия исправлена, где вы делаете _weakBs, чтобы указать на последний созданный объект B. Но я действительно не понимаю, в чем заключается трюк _weakBs.Add(b, _weakBs) при создании первого объекта B.   -  person CitizenInsane    schedule 07.01.2014
comment
@ CitizenInsane, Вау! Ты поймал меня. Я не помню почему... Более того, я не могу понять и думаю, что у меня есть несколько строк, которые можно/нужно удалить (возможно, какие-то тесты). Я добавил еще один образец, который, как мне кажется, лучше. Надеюсь, вы не найдете в нем еще одну странность??? ;-) !   -  person Eric Ouellet    schedule 07.01.2014


Ответы (3)


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

Если для финализаторов объекта будет необходимо использовать объекты WeakReference, объект должен предусмотреть некоторые условия, гарантирующие, что на эти объекты останутся строгие ссылки. Я не уверен, какой шаблон лучше всего подходит для этого, но ConditionalWeakTable<TKey,TValue>, добавленный в .net 4.0, может оказаться полезным. Это немного похоже на Dictionary<TKey,TValue>, за исключением того, что пока на саму таблицу строго ссылаются и на заданный ключ строго ссылаются, соответствующее ему значение будет считаться строго ссылающимся. Обратите внимание, что если ConditionalWeakTable содержит запись, связывающую X с Y и Y с таблицей, то пока X или Y остаются, таблица также остается.

person supercat    schedule 28.02.2013
comment
Моему ангелу... Большое спасибо!!! Именно такие люди, как вы, делают меня счастливым программистом. На самом деле быть человеком. Это дает мне некоторую надежду на человечество. Большое спасибо. Теперь я все понимаю... По крайней мере, я так думаю. (Длинный слабый - для условия, когда ситуация должна заботиться о коде, помеченном сборщиком мусора для удаления, но ожидающем завершения). Я не знал об ConditionalWeakTable, и это именно то, что я искал. Я мог бы сделать много артефактов, но это точно соответствует моей потребности. - person Eric Ouellet; 01.03.2013
comment
Думаю, на этот раз я тебя полностью понял. Это заняло некоторое время, но, по крайней мере, я сделал это!!! Еще раз спасибо !!! - person Eric Ouellet; 02.03.2013

Есть два аспекта сборки мусора, на которые вы не рассчитывали:

  • Точное время, когда WeakReference.IsAlive становится ложным. Ваш код неявно предполагает, что это произойдет при запуске финализатора. Это не так, это происходит, когда объект собирает мусор. После чего объект помещается в очередь финализатора, потому что у него есть финализатор и GC.SuppressFinalize() не вызывался, ожидая, пока поток финализатора выполнит свою работу. Таким образом, есть период времени, когда IsAlive имеет значение false, но ~B() еще не запущен.

  • Порядок, в котором завершаются объекты, непредсказуем. Вы неявно предполагаете, что B завершается до A. Вы не можете сделать это предположение.

Также есть ошибка в методе B.Dispose(): он неправильно подсчитывает экземпляры B, когда клиентский код явно удаляет объект. Вы еще не столкнулись с этой ошибкой.

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

person Hans Passant    schedule 26.02.2013
comment
Большое спасибо, Ганс, я ценю ваш ответ и подтверждаю, в чем была моя ошибка - мое предположение о достоверности цели WeakEvent и где она звучит так, как будто она была уничтожена. - person Eric Ouellet; 27.02.2013
comment
Конструктор для WeakReference принимает параметр, указывающий, должен ли он продолжать хранить ссылку на цель, которая стала подходящей для финализации, но все еще существует; несколько бесполезно, что эта опция будет работать только тогда, когда существует сильная ссылка на сам WeakReference. - person supercat; 06.02.2015

WeakReference _weakB доступен для сборки мусора одновременно с объектом a. У вас нет гарантии порядка здесь, поэтому вполне может быть, что _weakB завершается до объекта a.

Доступ к _weakB в финализаторе A опасен, так как вы не знаете состояние _weakB. Я предполагаю, что в вашем случае он был завершен, и это заставляет его возвращать значение null для .Target.

person Matt Smith    schedule 26.02.2013
comment
Один из способов проверить эту теорию — взять статическую ссылку на _weakB, чтобы она не могла быть собрана/финализирована сборщиком мусора до объекта a. - person Matt Smith; 26.02.2013
comment
@ Мэтт: Я думаю, вы также пропустили коллекцию, в которой находится сильная ссылка. Тогда b не подходит для GC при вызове финализатора a. - person Eric Ouellet; 26.02.2013
comment
@ Эрик, я ничего не сказал о b. Я сказал, что _weakB подходит для GC. Обратите внимание, что вы используете _weakB в финализаторе A, но _weakB может быть уже финализирован — и я предполагаю, что после финализации он вернет null для .Target (даже если объект b все еще жив) - person Matt Smith; 26.02.2013
comment
@ Matt --› Какая-то логика. Но это также безумие* и очень ограничительное. * сумасшедший, потому что это объект специального языка, и слабая ссылка должна быть допустимой. Это единственный способ гарантировать правильное и полное выпуск слабого события без использования таймера, опроса и/или других артефактов. - person Eric Ouellet; 26.02.2013
comment
@ Мэтт, наверное, ты прав. Я оставлю вопрос открытым, чтобы иметь возможность получить что-то, связанное с документацией, но я закрою его через день или два с вашим ответом (если нет лучшего - велик шанс, что он будет вашим), потому что это, вероятно, именно то, что произойдет ! Большое спасибо !!! Я также сообщу об ошибке и предложу Microsoft. - person Eric Ouellet; 26.02.2013
comment
Я не знаю, что вы подразумеваете под специальным языковым объектом. WeakReference — это класс, и он ведет себя последовательно — вы не знаете, в каком состоянии находится объект после его финализации. Похоже, у вас есть вопрос о чем-то другом (слабых событиях). Почему бы не задать этот вопрос и посмотреть, могут ли другие помочь вам найти решение. - person Matt Smith; 26.02.2013
comment
WeakReference не похож на обычный класс. Он имеет код, помеченный как InternalCall, который является специальным кодом. Также, на мой взгляд, этот класс должен быть определенным образом привязан к управлению GC/Memory. Хотя слабый финализатор мог быть вызван (и, вероятно, так и произошло), его ссылка все еще должна быть действительной. Это то, чего можно было бы ожидать от любого. В противном случае было бы очень и очень ограничительно во многих сценариях различных классов корневого дерева, которые имеют слабые отношения, но этот срок службы зависит от другого класса корневого дерева. - person Eric Ouellet; 26.02.2013
comment
Я вовсе не считаю это ограничительным — если вам нужна ссылка на что-то, сделайте ссылку на это. Задайте свой вопрос, который показывает ограниченность, о которой вы говорите, и вы, вероятно, получите лучшее решение. - person Matt Smith; 26.02.2013
comment
Мой вопрос был тот, который я задал. Мне нужно, чтобы The WeakRef был действителен в финализаторе. Да, я могу задать вопрос или написать статью в codeproject, объясняющую все причины необходимости валидности WeakRef в финализаторе, но мой босс не согласится со временем, необходимым для этого :-(... Иначе я бы это сделал. - person Eric Ouellet; 26.02.2013
comment
@EricOuellet: Если бы я разрабатывал WeakReference, я бы сделал финализатор длинной слабой ссылки, проверяющей, жива ли цель, и, если да, перерегистрируй себя для финализации, не аннулируя цель, полагая, что пока цель жива кому-то еще может быть интересен его ресурс. Как только цель умерла, финализатор может освободить GCHandle (и прекратить перерегистрацию себя). Единственная причина, по которой мертвые GCHandles не исчезают, заключается в том, чтобы гарантировать, что все их потребители знают, что они мертвы. Как только единственный прямой потребитель (WeakReference) узнает... - person supercat; 02.03.2013
comment
...что ручка мертва, сама ручка может быть освобождена. - person supercat; 02.03.2013