Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is a WeakReference useless in a destructor?

Consider the following code:

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

        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("And here's:" + a);
        GC.KeepAlive(a);
    }

    private static void CreateB(A a)
    {
        B b = new B(a);
    }
}

class A
{ }

class B
{
    private WeakReference a;
    public B(A a)
    {
        this.a = new WeakReference(a);
    }

    ~B()
    {
        Console.WriteLine("a.IsAlive: " + a.IsAlive);
        Console.WriteLine("a.Target: " + a.Target);
    }
}

With the following output:

a.IsAlive: False
a.Target:
And here's:ConsoleApp.A

Why is it false and null? A hasn't been collected yet.

EDIT: Oh ye of little faith.

I added the following lines:

Console.WriteLine("And here's:" + a);
GC.KeepAlive(a);

See the updated output.

like image 577
Bubblewrap Avatar asked Aug 18 '10 07:08

Bubblewrap


3 Answers

Updated answer for updated question.

With the new question, we go through the following steps.

  1. A and B alive and rooted, B.a alive and rooted via B.
  2. A alive, B not rooted and eligible for collection. B.a not rooted and eligible.
  3. Collection happens. B and B.a are each finalisable, so they are put on the finaliser queue. B is not collected because it is finalisable. B.a is not collected, both because it is finalisable, and because it is referenced by B which has not yet been finalised.
  4. Either B.a is finalised, or B is finalised.
  5. The other of B.a or B is finalised.
  6. B.a and B are eligibled for for collection.

(If B was finalised at point 4 it would be possible for it to be collected before point 5, as while B awaiting finalisation keeps both B and B.a from collection, B.a awaiting finalisation does not affect B's collection).

What has happened is that the order between 4 and 5 was such that B.a was finalised and then B was finalised. Since the reference a WeakReference holds to an object is not a normal reference, it needs its own clean-up code to release its GCHandle. Obviously it can't depend on normal GC collection behaviour, since the whole point of its references is that they don't follow normal GC collection behaviour.

Now B's finaliser is run, but since the behaviour of B.a's finaliser was to release its reference it returns false for IsAlive (or in .NET prior to 1.1 if I'm remembering the versions right, throws an error).

like image 186
Jon Hanna Avatar answered Sep 18 '22 08:09

Jon Hanna


The key problem in this is that you are accessing a reference field during a finalizer. The underlying problem is that the WeakReference itself has (or can be, unpredictably) already been collected (since collection order is non-deterministic). Simply: the WeakReference no longer exists, and you are query IsValid / Target etc on a ghost object.

So accessing this object at all is unreliable and brittle. Finalizers should only talk to direct value-type state - handles, etc. Any reference (unless you know it will always out-live the object being destroyed) should be treated with distrust and avoided.

If instead, we pass in the WeakReference and ensure that the WeakReference is not collected, then everything works fine; the following should show one success (the one where we've passed in the WeakReference), and one fail (where we've created the WeakReference just for this object, thus it is eligible for collection at the same time as the object):

using System;
class Program
{
    static void Main(string[] args)
    {
        A a = new A();
        CreateB(a);

        WeakReference weakRef = new WeakReference(a);
        CreateB(weakRef);
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.KeepAlive(a);
        GC.KeepAlive(weakRef);

        Console.ReadKey();
    }

    private static void CreateB(A a)
    {
        B b = new B(a);
    }
    private static void CreateB(WeakReference a)
    {
        B b = new B(a);
    }
}

class A
{ }

class B
{
    private WeakReference a;
    public B(WeakReference a)
    {
        this.a = a;
    }
    public B(A a)
    {
        this.a = new WeakReference(a);
    }

    ~B()
    {
        Console.WriteLine("a.IsAlive: " + a.IsAlive);
        Console.WriteLine("a.Target: " + a.Target);
    }
}

What makes you say it isn't collected? It looks eligible.... no field on a live object holds it, and the variable is never read past that point (and indeed that variable may well have been optimised away by the compiler, so no "local" in the IL).

You might need a GC.KeepAlive(a) at the bottom of Main to stop it.

like image 39
Marc Gravell Avatar answered Sep 18 '22 08:09

Marc Gravell


This is indeed a bit odd and I can't say, that I have the answer, but here's what I have found so far. Given your example, I attached WinDbg immediately before the call to GC.Collect. At this point the weak reference holds on to the instance as expected.

Following that I dug out the actual instance of WeakReference and set a data breakpoint on the reference itself. Proceeding from this point the debugger breaks at mscorwks!WKS::FreeWeakHandle+0x12 (which sets the handle to null) and the managed call stack is as follows:

OS Thread Id: 0xf54 (0)
ESP       EIP     
0045ed28 6eb182d3 [HelperMethodFrame: 0045ed28] System.GC.nativeCollectGeneration(Int32, Int32)
0045ed80 00af0c62 System.GC.Collect()
0045ed84 005e819d app.Program.Main(System.String[])
0045efac 6eab1b5c [GCFrame: 0045efac] 

This seems to indicate, that the call to GC.Collect in turn ends up modifying the weak reference as well. This would explain the observed behavior, but I can't say if this is how it will behave in all cases.

like image 24
Brian Rasmussen Avatar answered Sep 17 '22 08:09

Brian Rasmussen