9
votes

I have this piece of code

public class Publisher
{
    public event EventHandler SomeEvent;
}

public class Subscriber
{
    public static int Count;

    public Subscriber(Publisher publisher)
    {
        publisher.SomeEvent += new EventHandler(publisher_SomeEvent);
    }

    ~Subscriber()
    {
        Subscriber.Count++;
    }

    private void publisher_SomeEvent(object sender, EventArgs e)
    {
        // TODO
    }
}

In the Main method of my application I have

static void Main(string[] args)
{
    Publisher publisher = new Publisher();

    for (int i = 0; i < 10; i++)
    {
        Subscriber subscriber = new Subscriber(publisher);
        subscriber = null;
    }

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

    Console.WriteLine(Subscriber.Count.ToString());
}

If I run this, I will have 0 as output. If I remove event subscriptions from the code, I will got the expecting result – which is 10.

When GC.Collect() is being called, gc is being forced to start garbage collection. Because Subscriber has Finalize defined in it, GC will suspend collection until finalizequeue is empty – that is after all Subscription instances will call its Finalize() methods ( Please correct me if my assumptions are wrong). At the next line GC.WaitForPendingFinalizers() is called which will effectively suspend execution until finalizer queue is empty. Now, because we have 0 as output I believe Finalize() is not being called, which makes me believe that GC didn’t mark subscriber instances to be collected, thus Finalizer() methods are not being called.

So I have 2 questions

  1. Is my assumption right and event subscription prevents GC to mark subscriber instances to be collected?
  2. If so, it is because publisher holds reference to subscriber? (Garbage collector and event handlers)

My only guess is that since there are 10 instances of Subscriber that are referencing to the same publisher instance, when GC collection occurs, it sees that there are other references to publisher, thus it can’t be collected, and as a result all subscription instances alongside with publisher are being moved to the next generation, so garbage collection doesn’t occur nor Finalize() is being called at the time code execution reaches to Console.WriteLine(Subscriber.Count.ToString())

Am I right or am I missing something here?

2
Think about it this way: when Publisher.SomeEvent delegate is being called it will execute whatever is subscribed to it at the moment of the call. This means that the subscriber instances has to be alive while the subscription is active. That's why Publisher has to keep a reference to all the subscribers. Since subscribers are reachable GC can't collect them.Andrew Savinykh

2 Answers

4
votes

You are mis-identifying what is really going on, a very common trap in C#. You'll need to run the Release build of your test program and run it without the debugger (press Ctrl+F5). The way it will run on your user's machine. And now notice that it completely doesn't matter anymore whether or not you subscribe the event, you will always get 10.

The issue is that, when you use a debugger, the publisher object doesn't get collected. I explained the reason for that in detail in this answer.

Expanding a bit on that, you have circular references here. The Subscriber objects reference the Publisher object. And the Publisher object has references to the Subscriber objects. Circular references are not sufficient to keep objects alive. And thank goodness for that, garbage collection would not be very effective if that was the case. The publisher object must be referenced elsewhere to stay alive, the local variable isn't good enough.

3
votes

The answer to both questions is Yes.