0

Bear with me as I'm doing "interesting stuff" here with enumerators and LINQ. And while checking if things get properly cleaned up, I noticed something within a simple console application that I created to test a few things. But first, some pieces of code, starting with the generic DoAll() method:

public static IEnumerable<T> DoAll<T>(this IEnumerable<T> data, Action<T> action)
{
    foreach (var item in data)
    {
        action(item);
        yield return item;
    }
}

This just does a specific action with an item before yielding it to the next method in the pipeline. Next, a base IDisposable object that just serves as example:

public class Dummy : IDisposable
{
    private static int _count = 0;
    public int Count = ++_count;
    public Dummy() => Console.WriteLine($"Dummy {Count} created.");
    ~Dummy() => Console.WriteLine($"Dummy {Count} destroyed.");
    public void Dispose() => Console.WriteLine($"Dummy {Count} disposed.");
    private int _value = 0;
    public int Value => ++_value;
}

The Dummy is simply an object that counts how often one has been created and a value that keeps track of how often it was called. As I said, simple example. Now I need an enumerator! This one:

public static class DummyList
{
    public static IEnumerable<int> GetDummy()
    {
        using (var dummy = new Dummy())
        {
            while (true) { yield return dummy.Value; }
        }
    }
}

No rocket science here. Just a static class with a static extension method that will call the Dummy Value method forever in a loop, yielding each value to the next method in the pipeline.
Just to be clear, while this seems an infinite loop, the loop will get broken in the next method. And the next method is a test method:

public static void DummyTest()
    {
        for (var x = 0; x < 5; x++)
        {
            Console.WriteLine(
                string.Join(", ", 
                    DummyList.GetDummy()
                             .DoAll(i => Console.Write($"Got {i}! "))
                             .Take(10)
                             .Select(i => i.ToString())));
        }
        GC.Collect();
    }

Well, the above code will use my enumerator 5 times, each time taking just 10 values before writing it as a list to the console. And at the end of it all, I call the garbage collector to clean it up. And I just call DummyTest(); from my main method in my console application to get this result:

Dummy 1 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 1 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 2 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 2 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 3 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 3 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 4 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 4 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 5 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 5 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 4 destroyed.
Dummy 3 destroyed.
Dummy 2 destroyed.
Dummy 1 destroyed.

Well, it almost looks okay. Every time the object gets created and disposed so I know my enumeration will get cleaned up, even though I break it off during the loop. I wanted to make sure this would happen and I first wanted to ask here if it would. But this test shows it does.
However, the garbage collector only destroys 4 of the 5 objects! And that troubles me a bit. All this code to show my enumerator will clean up nicely, only to discover that it won't clear up everything. And while it's not that important, I do want to know why it won't delete the last object and how I can force the garbage collector to destroy them all in this complex example.

So why isn't the last object destroyed and how can I force it's destruction within the DummyTest method?


Things are getting a bit more complicated. This error only happens in "Debug" mode, not "Release" mode. I'm not debugging it when it skips Dummy 5, but I have compiled it with and without debug information. Without debug information Dummy 5 gets freed. Something seems to hold onto that Dummy when the project is compiled with debug information.
I want to know why, and how to prevent this.

Wim ten Brink
  • 25,901
  • 20
  • 83
  • 149
  • What happens if you call [GC.WaitForPendingFinalizers](https://learn.microsoft.com/en-us/dotnet/api/system.gc.waitforpendingfinalizers) after the call to `GC.Collect`? – Paulo Morgado Oct 31 '19 at 08:45
  • @PauloMorgado, it doesn't make a difference. For some reason, Dummy 5 isn't finalized. It only gets finalized if I put GC.Collect() after the DummyTest() method call in main(). While it is disposed, it's not finalized inside the method. – Wim ten Brink Oct 31 '19 at 20:50
  • 1
    I tried it ou on [LINQPad](https://linqpad.net/) and it prints `Dummy 5 destroyed.`. Without the call to `GC.WaitForPendingFinalizers`. – Paulo Morgado Nov 03 '19 at 21:23
  • Interesting, but LINQPad isn't the same as a console application. Does it print it for or after the other four dummies? Because I suspect that LINQPad is calling the garbage collector again after the DummyTest() method is executed. If I put the GC.Collect() in main after this function call then dummy 5 will be destroyed after the other four. But I want it gone within the DummyTest method. – Wim ten Brink Nov 04 '19 at 01:08
  • 1
    Interestingly, this only happens in debug mode, not release mode. Interesting... :-O – Wim ten Brink Nov 04 '19 at 19:23
  • 2
    Sounds like the debugger or the IDE hanging on to the enumerator; `.Current` is undefined at the end of a sequence, and "still refs the last value" is perfectly valid. Debug builds aren't useful for checking things like this, frankly. – Marc Gravell Nov 04 '19 at 19:46
  • It's not the debugger or IDE as I execute it without debugging. These kind of chained methods are a bit challenging to debug. But CTRL+F5 and either dummy is freed when it's a release build or not freed if it's a debug build. – Wim ten Brink Nov 04 '19 at 22:00
  • 1
    @Marc's comment still applies. The bigger difference is debug vs release build. When running the former, the GC isn't as aggressive, and will consider an object reachable as long as there's any stack (local) variable referencing it. In a release build, the GC will consider the same exact object unreachable if there is no code left in the method that could execute which references that variable. That easily could explain your observed difference. – Peter Duniho Nov 04 '19 at 23:30
  • 1
    Note that this question is not _about_ C# 6, nor enumeration per se. You could reproduce the same basic behavior with much simpler code, and no enumerables involved. – Peter Duniho Nov 04 '19 at 23:32
  • Maybe it could be done with simpler code, but I encounter it when I create an infinite enumeration. It is to make sure that even in this specific situation, everything gets cleaned up. It's the DummyList() method that makes me suspicious. – Wim ten Brink Nov 05 '19 at 02:59

1 Answers1

1

It seems the reason is related to the garbage collector, which behaves different in debug mode than in release mode. As some have commented, the garbage compiler might still hold a reference to Dummy 5 when the DummyTest() method closes, as it would keep the value for debugging purposes. In release mode, the garbage collector behaves more aggressively.
This question has similarities with Why C# Garbage Collection behavior differs for Release and Debug executables? and answered in Does garbage collection run during debug? but this question is still interesting as it applies to a situation where a resource is part of a more complex infinite enumeration.
This is not a memory leak, though! It's just that when the project is compiled in debug mode, the garbage collector tends to hang on a bit longer to some resources until after the method is closed, as any running debugger might still be evaluating the value. (It doesn't know that there's no debugger running.)

Wim ten Brink
  • 25,901
  • 20
  • 83
  • 149