C# WeakReference object is NULL in finalizer although still strongly referenced
Asked Answered
S

3

5

Hi I have code here where I don't understand why I hit the breakpoint (see comment).

Is this a Microsoft bug of something I don't know or I don't understand properly ?

The code was tested in Debug but I think it should not changes anything.

Note: You can test the code directly in a console app.

JUST FOR INFORMATION... following supercat answer, I fixed my code with proposed solution and it works nicely :-) !!! The bad thing is the usage of a static dict and the performance the goes with it but it works. ... After few minutes, I realized that SuperCat give me all hints to do it better, to workaround the static dictionary and I did it. Code samples are:

  1. Code with the bug
  2. Code corrected but with a static ConditionalWeakTable
  3. Code with ConditioalWeakTable that include the SuperCat tricks (thanks so much to him !)

Samples...

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();
    }
    }

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

Version corrected:

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);
        }
    }

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

Code with ConditioalWeakTable that include the SuperCat tricks (thanks so much to him !)

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);
        }
    }

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

}

Following question of CitizenInsane... I don't remember exactly why I did what I did... I found my sample but wasn't sure about my intention at that time. I tried to figure it out and came with the following code which I thing is more clear but still don't remember my original need. Sorry ???

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
        }
    }

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

}
Sheff answered 26/2, 2013 at 16:5 Comment(13)
Please clarify is this release or debug mode. Also, the comment says "B is still referenced", but B is a type. Did you mean b?Lignite
Could it not be because when the breakpoint is triggered b is null, so the GC collects it anyway? I'm not certain mind.Eucaine
All bets are off in the finalizer.Racy
@ Brian Rasmussen: clarifications done, thanksSheff
@EricOuellet Thanks. Debug/release is important in this case because the release mode GC may consider objects to be eligible for collection as soon as they are no longer accessed within a function.Lignite
@ leppie: I understand that finalizers are special, is it written somewhere (ECMA) or elsewhere that I can't do that. It's very restrictive if I can't ? If it's undocumented, it's a bug to me. But I want to make sure before reporting something.Sheff
I would go with what Brian is saying :)Racy
Another question: Are you saying that you believe that the object b points to should be alive at the point of Debugger.Break? That would not be the case as you only get there if b was null in the first place.Lignite
@ Brian, objects should not be eligible for GC if hard reference still exists which is the case here as you can see in the sample where the static collection hold that ref. Static is a root for the GC and then my object has never been garbage collected... I only loses my WeakrefSheff
DOH! Too much code. I didn't even see the list until now. Sorry about that.Lignite
@ Dan, I'm really not sure it is only for unmanaged resources. I'm pretty sure that it is also applying to WeakEvent cleanup. If I ever have spare time, I will write an article about that in CodeProject. See codeproject.com/Articles/29922/Weak-Events-in-C from Daniel Grunwald. Althought very nice implemantation, it is leaking the weak data to the hanler in it's SmartWeakEvent that could be fixed by a SmartHandler with a finalizer that advise it's source of dying. My situation is similar to this one.Sheff
@Eric, could you please clarify for what "SuperCat trick" is ? I understood the "Version corrected" where you make _weakBs to point to the last created B object. But I truly don't get what does the trick _weakBs.Add(b, _weakBs) when first B object is created.Pisano
@ CitizenInsane, Wow! You got me. I don't remember why... Moreover, I can't figure it out and think that I have few lines that could/should be removed (perhaps some tests). I added another sample that I think is better. I hope you will not find another weird thing in it ??? ;-) !Sheff
T
6

A sometimes-irksome limitation of WeakReference is that a WeakReference may be invalidated if no strongly-rooted reference exists to the WeakReference itself, and this may occur even if the trackResurrection constructor parameter was true, and even if the target of the WeakReference is strongly rooted. This behavior stems from the fact that a WeakReference has an unmanaged resource (a GC handle) and if the finalizer for the WeakReference didn't clean up the GC handle, it would never get cleaned up and would constitute a memory leak.

If it will be necessary for an object's finalizers to make use of WeakReference objects, the object must make some provision to ensure that those objects remain strongly referenced. I'm not sure what the best pattern is to accomplish this, but the ConditionalWeakTable<TKey,TValue> that was added in .net 4.0 may be useful. It's a little bit like Dictionary<TKey,TValue> except that as long as a table itself is strongly referenced and a given key is strongly referenced, its corresponding value will be regarded as strongly referenced. Note that if a ConditionalWeakTable holds an entry linking X to Y, and Y to the table, then as long as X or Y remains, the table will remain as well.

Teat answered 28/2, 2013 at 23:57 Comment(2)
To my angel... Hé thanks a lot !!! It's people like you that make me happy to be a programmer. In fact, to be human. It give me some hope in humanity. Thank you so much. I understand every thing now... At least I think. (Long weak is for condition where situation should care of code marked for deletetion by the GC but waiting to be finalized). I didn't know for the ConditionalWeakTable and it is exactly what I was looking for. I could have done many artifacts but that exactly fits my need.Sheff
I think I got you completely this time. It took a while but at least I did it !!! Thannks again !!!Sheff
U
5

There are two aspects of garbage collection that you didn't count on:

  • The exact time at which the WeakReference.IsAlive becomes false. Your code implicitly assumes that will happen when the finalizer runs. This is not the case, it happens when the object gets garbage collected. After which the object is placed on the finalizer queue, because it has a finalizer and GC.SuppressFinalize() wasn't called, waiting for the finalizer thread to do its job. So there's a period of time where IsAlive is false but ~B() hasn't run yet.

  • The order in which objects get finalized is not predictable. You implicitly assume that B is finalized before A. You cannot make this assumption.

There's also a bug in the B.Dispose() method, it won't correctly count B instances when the client code explicitly disposed the object. You haven't hit that bug yet.

There is no reasonable way to fix this code. Moreover, it tests something that is already backed by hard guarantees provided by the CLR. Just remove it.

Ursine answered 26/2, 2013 at 19:36 Comment(2)
Thanks a lot Hans, I appreciate your answer and confirm me what was my error - my assumption of the validity of the WeakEvent - "target" and where is sounds like it has be destroyed.Sheff
The constructor for WeakReference accepts a parameter which indicates whether it should continue to hold a reference to a target which has become eligible for finalization but still exists; somewhat unhelpfully that option will only work usefully when a strong reference exists to the WeakReference itself.Teat
G
2

The WeakReference _weakB is available for garbage collection at the same time as the object a is. You don't have a guarantee of order here, so it could very well be that _weakB is finalized before object a.

Accessing _weakB in the finalizer of A is dangerous, since you don't know the state of _weakB. I'm guessing that in your case it has been finalized, and that that is causing it to return null for .Target.

Globin answered 26/2, 2013 at 16:36 Comment(11)
One way to test this theory out is to take a static reference to _weakB so that it cannot be garbage collected/finalized before obect aGlobin
@ Matt: I think you also missed out the collection where reside the strong reference. Then b is not eligible for GC when the "a" finalizer is called.Sheff
@Erik, I said nothing of b. I said the _weakB is eligible for GC. Notice you are using _weakB in the finalizer of A, but _weakB might already be finalized--and I'm guessing that once finalized, it will return null for .Target (even though object b is still live)Globin
@ Matt --> Kind of logic. But it is also crazy* and very restrictive. * crazy because it is a special langage object and the weak reference should be valid. That is the only way to ensure to release a weak event properly and completely without any timer, polling and/or other artifacts.Sheff
@ Matt, you are probably right. I will let the question open to have a chance to get something related to documentation but I will close it in a day or two with your answer (if none better - lot of chance that it will be yours) because it is probably what exactly happen ! Thanks a lot !!! I will also report a bug and a suggestion to Microsoft.Sheff
I don't know what you mean by "special language object". WeakReference is a class, and it is behaving consistently--you don't know what state an object is in after its been finalized. It sounds like you have a question about something else (weak events). Why not ask that question and see if others can help you find a solution.Globin
WeakReference is not like an ordinary class. It has code marked as "InternalCall" which is special code. Also to my opinion, this class should be tied to the GC/Memory management in a certain way. Although Weak finalizer could have been called (and its probably what happen), its reference should still be valid. Its what would be expected by anybody. Doing otherwise would be very very restricitive in many scenarios of different rooted tree classes that have weak relations but that lifespan depends on different rooted tree class.Sheff
I don't see it as being restrictive at all--if you need a reference to something, take a reference to it. Ask your question that shows the restrictiveness your talking about, and you'll probably get a better solution.Globin
My question was the one I asked. I need The WeakRef to be valid in finalizer. Yes I can ask a question or write an article in codeproject explaining all the reasons behind the necessity of WeakRef validity in finalizer but my boss would not agree with the time needed to do so :-(... Otherwise I would do it.Sheff
@EricOuellet: If I were designing WeakReference, I would have made the finalizer of a long weakreference check whether the target is alive and, if so, re-register itself for finalization without invalidating the target, figuring that as long as the target is alive someone might still be interested in its resource. Once the target has died, the finalizer could free up the GCHandle (and stop re-registering itself). The only reason dead GCHandles stick around is to ensure that all their consumers know they're dead. Once the only direct consumer (the WeakReference) knows...Teat
...that the handle is dead, the handle itself may be freed.Teat

© 2022 - 2024 — McMap. All rights reserved.