Обектът C# WeakReference е NULL във финализатора, въпреки че все още е силно препратен

Здравейте, имам код тук, където не разбирам защо ударих точката на прекъсване (вижте коментара).

Дали това е грешка на Microsoft за нещо, което не знам или не разбирам правилно?

Кодът беше тестван в Debug, но мисля, че не трябва да променя нищо.

Забележка: Можете да тествате кода директно в конзолно приложение.

САМО ЗА ИНФОРМАЦИЯ... след отговора на supercat, поправих кода си с предложеното решение и работи добре :-) !!! Лошото е използването на статичен dict и производителността върви с него, но работи. ... След няколко минути разбрах, че 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 е нула, така че GC го събира така или иначе? Не съм сигурен ум.   -  person Kobunite    schedule 26.02.2013
comment
Всички залози са изключени във финализатора.   -  person leppie    schedule 26.02.2013
comment
@ Браян Расмусен: разясненията са направени, благодаря   -  person Eric Ouellet    schedule 26.02.2013
comment
@EricOuellet Благодаря. Отстраняването на грешки/освобождаването е важно в този случай, тъй като режимът на освобождаване GC може да счита обектите за отговарящи на условията за събиране веднага щом вече няма достъп до тях в рамките на функция.   -  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 е било null на първо място.   -  person Brian Rasmussen    schedule 26.02.2013
comment
@ Брайън, обектите не трябва да отговарят на условията за GC, ако все още съществува твърда препратка, какъвто е случаят тук, както можете да видите в пробата, където статичната колекция държи тази препратка. Static е корен за GC и след това моят обект никога не е бил събиран за боклук... Губя само своя Weakref   -  person Eric Ouellet    schedule 26.02.2013
comment
DOH! Твърде много код. Дори не видях списъка до сега. Съжалявам за това.   -  person Brian Rasmussen    schedule 26.02.2013
comment
@ Дан, наистина не съм сигурен, че е само за неуправляеми ресурси. Почти съм сигурен, че се прилага и за почистване на WeakEvent. Ако някога имам свободно време, ще напиша статия за това в CodeProject. Вижте codeproject.com/Articles/29922/Weak-Events-in-C от Даниел Грюнвалд. Въпреки че е много хубава реализация, тя изпуска слабите данни към hanler в своя SmartWeakEvent, което може да бъде коригирано от SmartHandler с финализатор, който уведомява източника на умиране. Моята ситуация е подобна на тази.   -  person Eric Ouellet    schedule 27.02.2013
comment
@Eric, можеш ли да изясниш какъв е трикът на 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
На моя ангел... Hé благодаря много !!! Хората като теб ме правят щастлив, че съм програмист. Всъщност да бъде човек. Това ми дава известна надежда в човечеството. Много благодаря. Вече разбирам всичко... Поне така си мисля. (Дългият слаб е за условие, при което ситуацията трябва да се грижи за код, маркиран за изтриване от GC, но чакащ да бъде финализиран). Не знаех за ConditionalWeakTable и това е точно това, което търсех. Можех да направя много артефакти, но това точно отговаря на нуждите ми. - person Eric Ouellet; 01.03.2013
comment
Мисля, че този път те разбрах напълно. Отне известно време, но поне го направих !!! Благодаря отново!!! - person Eric Ouellet; 02.03.2013

Има два аспекта на събирането на боклука, на които не сте разчитали:

  • Точното време, в което WeakReference.IsAlive става невярно. Вашият код имплицитно предполага, че това ще се случи, когато се стартира финализаторът. Това не е така, случва се, когато обектът бъде извозен за боклук. След което обектът се поставя в опашката на финализатора, тъй като има финализатор и GC.SuppressFinalize() не е извикан, чакайки нишката на финализатора да свърши работата си. Така че има период от време, в който IsAlive е невярно, но ~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
@Erik, не казах нищо за b. Казах, че _weakB отговаря на условията за GC. Забележете, че използвате _weakB във финализатора на A, но _weakB може вече да е финализиран - и предполагам, че след като бъде финализиран, той ще върне null за .Target (въпреки че обект b все още е активен) - person Matt Smith; 26.02.2013
comment
@ Мат --› Някаква логика. Но също така е луд* и много ограничаващ. * луд, защото това е обект на специален език и слабата препратка трябва да е валидна. Това е единственият начин да се гарантира правилното и пълно освобождаване на слабо събитие без таймер, запитване и/или други артефакти. - 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 по определен начин. Въпреки че Weak finalizer може да бъде извикан (и вероятно това ще се случи), неговата препратка все още трябва да е валидна. Това, което би се очаквало от всеки. Противното би било много ограничително в много сценарии на различни вкоренени дървесни класове, които имат слаби връзки, но продължителността на живота зависи от различни вкоренени дървесни класове. - person Eric Ouellet; 26.02.2013
comment
Изобщо не го виждам като ограничаващо - ако имате нужда от препратка към нещо, вземете препратка към него. Задайте въпроса си, който показва рестриктивността, за която говорите, и вероятно ще получите по-добро решение. - person Matt Smith; 26.02.2013
comment
Моят въпрос беше този, който зададох. Трябва ми 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