2

I've read this article and this too.

I was trying to implement destructor in simple code.

class Program
{
    static void Main(string[] args)
    {
        CreateSubscriber();
        Console.Read();
    }

    static void CreateSubscriber()
    {
        Subscriber s = new Subscriber();
        s.list.Clear();
    }
}

public class Subscriber
{
    public List<string> list = new List<string>();
    public Subscriber()
    {
        for(long i = 0; i < 1000000; i++)
        {
            list.Add(i.ToString());
        }
    }

    ~Subscriber()
    {
        //this line is only performed on explicit GC.Collect()
        Console.WriteLine("Destructor Called - Sub");
    }
}

As when code reached line of Console.Read(), instance of Subscriber was no longer in scope (I was expecting it to be eligible for Garbage collection). I left above code running for almost 2 hours waiting for destructor of Subscriber. but that never called, neither memory taken by the code released.

I understood, in c# we cannot call destructors programmatic and it is automatically called on Garbage collection , So I tried to call GC.Collect() explicitly.

By doing that, I could see that destructor was called. So In my above code, Garbage collection was not being done! But Why?

Is it because, program is single threaded and that thread is waiting for user input on Console.Read() ?

Or it does have something with List of string ? if so what is it


Update (for future readers)

as Fabjan suggested in his answer

Most likely somewhere when a new object is created and memory is allocated for it, GC does perform a check of all references and collects the first object.

and suggested to try

CreateSubscriber();
Console.Read();
CreateSubscriber();
Console.Readkey();

I updated the code like below,

class Program
{
    static void Main(string[] args)
    {
        CreateSubscriber(true);
        Console.ReadLine();
        CreateSubscriber(false);
        Console.ReadLine();
    }
    static void CreateSubscriber(bool toDo)
    {
        Subscriber s = new Subscriber(toDo);
        s.list.Clear();
    }
}
public class Subscriber
{
    public List<string> list = new List<string>();
    public Subscriber(bool toDo)
    {
        Console.WriteLine("In Consutructor");
        if (toDo)
        {
            for (long i = 0; i < 5000000; i++)
                list.Add(i.ToString());
        }
        else
        {
            for (long i = 0; i < 2000000; i++)
                list.Add(i.ToString());
        }
        Console.WriteLine("Out Consutructor");
    }
    ~Subscriber()
    {
        Console.WriteLine("Destructor Called - Sub");
    }
}

output:

enter image description here

and as he expected, on second instance creation of Subscriber, i could see GC being collected (finalizer being called).

Note that: in else condition of constructor of Subscriber, i m adding less items in list then if condition - to notice if RAM usage of application is being decreased accordingly, Yes it is being decreased too.

there in else condition, i could have left list of string empty (so memory usage will be significantly decreased). But doing so, GC is not being collected. Most likely because of the reason which M.Aroosi has mentioned in question's comment.

in addition to what's said above, the GC will only collect once a generation gets full(or due to an explicit call), and just 1 object created wouldn't trigger it. Yes the object is elligible for finalization, but there's no reason for the GC to collect it.

Amit
  • 1,821
  • 1
  • 17
  • 30
  • 3
    First off, that's a finalizer, not a destructor. When you have an explicit finalizer, objects go into the finalization queue,which I believe means it takes one additional GC (I might have that part wrong). That said, finalization is not deterministic, meaning there is no guarantee to when (or if?) your finalizer will be called. Going out of scope doesn't automatically call your finalizer. If you want determinism, use the IDisposable interface/disposable pattern. – pinkfloydx33 Jun 09 '18 at 14:49
  • @pinkfloydx33 great pointing about Disposing, it is also there in mentioned articles which i have refereed. I have tried that, and it works too. But i was thinking how destructors would work ? – Amit Jun 09 '18 at 14:54
  • 1
    Also note that in this case there is no need for a finalizer (or disposable for that matter). There's no unmanaged resources to be freed. The runtime/GC thread will handle the cleanup of your class once it's no longer referenced from anywhere. The problem here is that you've explicitly defined a finalizer which is part of what's actually keeping it from being collected in the first place (at least when/where you're expecting it) – pinkfloydx33 Jun 09 '18 at 14:54
  • 1
    In addition to what's said above, the GC will only collect once a generation gets full(or due to an explicit call), and just 1 object created wouldn't trigger it. Yes the object is elligible for finalization, but there's no reason for the GC to collect it. – Mor A. Jun 09 '18 at 14:54
  • @M.Aroosi I was expecting that to be a reason, as my this code is not putting that load on heap! – Amit Jun 09 '18 at 14:56
  • Eric Lippert has some insights on this topic as well https://stackoverflow.com/a/44573731/578411 – rene Jun 09 '18 at 15:00
  • @rene I am reading that, but one difference i found that, in his code, Calculator's object was in scope while in mine it is not. – Amit Jun 09 '18 at 15:02
  • 1
    @pinkfloydx33: Both "finalizer" and "destructor" are acceptable. The C# spec calls them destructors, the CLR spec calls them finalizers. The mismatch was likely due to a desire in the early days of C# to make syntax that was appealing to C++ programmers. – Eric Lippert Jun 11 '18 at 18:44
  • 2
    Original poster: the takeaway you should be getting here is **you do not need to write a destructor**. Writing a destructor is **almost never the right thing to do in C#** and you need to be an **expert** on the C# memory model in order to write a correct destructor. If you think you need a destructor, your are probably wrong; why do you think you need a destructor? – Eric Lippert Jun 11 '18 at 18:46
  • 1
    Note also that **your belief that garbage collection triggers finalization is false**. Garbage collection can put an object onto the finalizer queue but **the finalizer queue is not guaranteed to be emptied when a collection happens**. If you **for testing purposes** need to ensure that a finalizer runs then you need to call both `Collect` and `WaitForPendingFinalizers`. But again **if you are doing that in production code then you are doing something wrong**. You should not ever rely on a finalizer running at any particular time. – Eric Lippert Jun 11 '18 at 19:00
  • 1
    Note also that the codeproject article you linked to, while substantively correct, does have some small errors, and it fails to make the strong case that writing a correct destructor is very difficult. In particular, the line "*GC ensures that objects, which are being referenced, are not destroyed*" is technically correct but fails to point out many subtle issues: **it is not clear when an object is "being referenced"**, and often there are objects that you think are rooted that are not. It is possible, for instance, for an object to be unrooted *in its own constructor*! – Eric Lippert Jun 11 '18 at 19:05
  • @EricLippert good to know, though I think I'll stick to *not* using the term destructor. It's been a pet peeve of mine when it's referred to as such, mostly because it seems to invoke the idea that determinism is somehow involved. It's quite unfortunate that the syntax is similar, or rather that it is recognized by those new to the language, but familiar with c++, as something it is not. – pinkfloydx33 Jun 11 '18 at 22:20
  • @pinkfloydx33: Indeed, the situation is regrettable. I agree with your opinion that it was a mistake to make something that *looked* like a C++ destructor but with semantics *just different enough to cause confusion*. In my informal unscientific polling I've found that most developers do not have any particular feeling that "destructor" has different semantics than "finalizer", but of the sizable fraction who do, they agree with your characterization: that "destructor" implies deterministic finalization and "finalizer" implies best-effort, non-deterministic delayed finalization. – Eric Lippert Jun 11 '18 at 22:24
  • 1
    @pinkfloydx33: Given how hard it is to write a correct finalizer, and given the reasonable requirement that objects clean up deterministically, I wish that C# had chosen to make `~Foo()` a syntactic sugar for "create a standard implementation of `IDisposable`", and made some completely other *extremely scary* syntax for the rare cases when you need to implement `Finalize`. The compiler could then complain if an object that implements `~Foo` was not used in a `using` context, and so on; it would have prevented a lot of bugs all around. Such is hindsight some decades in to the project. :) – Eric Lippert Jun 11 '18 at 22:27
  • @EricLippert I actually don't need (and haven't used) destructors in my code (although there were cases where we have used Dispose to release some resources). I actually wanted to try this concept and came here as it wasn't behaving as per my expectations ! However thanks of information sharing. – Amit Jun 12 '18 at 15:43

1 Answers1

2

As when code reached line of Console.Read(), instance of Subscriber was no longer in scope (I was expecting it to be eligible for Garbage collection).

When GC detects that the reference for the instance of Subscriber is lost (out of scope) it will mark this object to be collected on one of the next rounds. But only GC knows when exactly will this next round be.

Is it because, program is single threaded and that thread is waiting for user input on Console.Read() ?

No, if we run this code in a separate thread the result will be the same. However if we change this:

CreateSubscriber();
Console.Read();

To:

CreateSubscriber();
Console.Read();
CreateSubscriber();
Console.Readkey();

We could see that GC will collect the garbage and run the finalizer after Console.Read(). Why?

Most likely somewhere when a new object is created and memory is allocated for it, GC does perform a check of all references and collects the first object.

Let's summarize it a bit:

  • When we only create an object and there is no reference in code that points to this obj or its class until program ends - GC allows program to end and collect the garbage before exiting.

  • When we create an object and there is some sort of reference to obj or its class - GC does performs a check and collects the garbage.

There's some complex logic behind how and when does the GC run collect and how and when the lifetime of the obj ends.

A quote from Eric Lippert's answer:

The lifetime may be extended by a variety of mechanisms, including capturing outer variables by a lambda, iterator blocks, asynchronous methods, and so on

It's very rare that we need to execute some code on object's destruction. In that specific scenario instead of guessing when the obj will be destroyed we could run GC.Collect explicitly.

More often though we might need to free some managed resources and for that we could use IDisposable interface and using statement that will automatically call Dispose before the control flow leaves the block of code (it'll create a try {} finally {} clause and in finally it will call Dispose for us).

using(myDisposable) 
{ 
   ... 
}  // dispose is called here
Fabjan
  • 13,506
  • 4
  • 25
  • 52