-1

If I run the simple method on Unit Test, which generates instances using unmanaged resource, for example bitmap, memory keeps increasing. In the case of executing the method in Thread or ThreadPool, the result is same. But if I run using Task.Run(), GC works properly and memory is not increased.

What is the difference of GC operation between Task.Run and others context?

    [TestMethod]
    public void Test()
    {
        //1. GC didn't work.
        Work();

        //2. GC didn't work.
        //new Thread(() => 
        //{
        //    Work();
        //}).Start();

        //3. GC didn't work.
        //ThreadPool.QueueUserWorkItem((obj) =>
        //{
        //    Work();
        //});

        //4. GC didn't work
        //new Action(() =>
        //{
        //    Work();
        //}).BeginInvoke(null, null);

        //5. GC works.
        //Task.Run(() =>
        //{
        //    Work();
        //});

        Thread.Sleep(Timeout.Infinite);
    }

    private void Work()
    {
        while (true)
        {
            GC.Collect();

            var bitmap = new Bitmap(1024, 768);
            Thread.Sleep(10);
        }
    }

First case memory log.

PrivateBytes : 282.0MB, AllHeapsBytes : 3.5MB, Thread Count : 32, CPU Usage : 12% PrivateBytes : 499.0MB, AllHeapsBytes : 2.8MB, Thread Count : 33, CPU Usage : 16% PrivateBytes : 734.0MB, AllHeapsBytes : 2.9MB, Thread Count : 33, CPU Usage : 11% PrivateBytes : 959.0MB, AllHeapsBytes : 3.0MB, Thread Count : 33, CPU Usage : 14% PrivateBytes : 1173.0MB, AllHeapsBytes : 3.1MB, Thread Count : 33, CPU Usage : 10% PrivateBytes : 1389.0MB, AllHeapsBytes : 2.9MB, Thread Count : 33, CPU Usage : 12% PrivateBytes : 1597.0MB, AllHeapsBytes : 2.9MB, Thread Count : 33, CPU Usage : 9% Exception thrown: 'System.ArgumentException' in System.Drawing.dll An exception of type 'System.ArgumentException' occurred in System.Drawing.dll but was not handled in user code Parameter is not valid.

5th case memory log

PrivateBytes : 41.0MB, AllHeapsBytes : 3.5MB, Thread Count : 32, CPU Usage : 16% PrivateBytes : 41.0MB, AllHeapsBytes : 2.9MB, Thread Count : 33, CPU Usage : 13% PrivateBytes : 41.0MB, AllHeapsBytes : 2.9MB, Thread Count : 33, CPU Usage : 14% PrivateBytes : 41.0MB, AllHeapsBytes : 2.9MB, Thread Count : 33, CPU Usage : 12% PrivateBytes : 41.0MB, AllHeapsBytes : 2.9MB, Thread Count : 33, CPU Usage : 14%

--- Updated ---

GC.Collect() doesn't collect immediately. When I call GC.Collect(), the GC will run each object's finalizer on a separate thread.

For GC working immediately, I should add GC.WaitForPendingFinalizers() after calling GC.Collect().

https://www.developer.com/net/csharp/article.php/3343191/C-Tip-Forcing-Garbage-Collection-in-NET.htm

But still I don't understand why GC works in Task.Run context.

--- Updated 2 ---

When I run this code in simple winform project, GC works properly and memory does not grow.

Bongho Lee
  • 81
  • 6
  • Why not just use using or Dispose? i know this wasn't an answer, i'm just wondering why are you relying on garbage collection – TheGeneral Jan 02 '18 at 09:55
  • 3
    I copied your `Work` method, tried it with and without that sleep in there, and I don't get an error (though I have only been running it for 2 minutes). Please explain which error you're getting or what "didn't work" actually means. The two versions are running side by side and neither uses more than 18mb of memory (though they go up and down a bit). – Lasse V. Karlsen Jan 02 '18 at 09:56
  • 4
    However, be aware of one thing. If your error relates to GDI handles then yes, that is a thing and though it is related to how GC does its job, it's not entirely related. The problem is that GC is not aware of unmanaged handles, such as GDI objects, so if Bitmap allocates one of those, you might eventually run out of such handles, even though memory is capable of handling that many Bitmap instances before running a GC. The solution to this problem is to dispose of bitmaps once you're done with them but again, this is just a guess **because we still don't know what "didn't work" means** – Lasse V. Karlsen Jan 02 '18 at 09:59
  • 1
    'Parameter is not valid' does not point to the GC – H H Jan 02 '18 at 10:03
  • In the case of 1~4, exception occurs in 10 seconds. In the case of Task.Run(), GC collects bitmap memory and the exception does not occur. – Bongho Lee Jan 02 '18 at 10:04
  • You can't conclude anything about the GC from 'memory is increased'. How did you measure that anyway? – H H Jan 02 '18 at 10:04
  • 1
    what if you call WaitForPendingFinalizers ? – TheGeneral Jan 02 '18 at 10:06
  • @HenkHolterman I used timer and Process.GetCurrentProcess().PrivateMemorySize64 for PrivateBytes. – Bongho Lee Jan 02 '18 at 10:09
  • @Saruman after calling WaitForPendingFinalizers(), the problem is solved. But I don't know what is the difference between Task and others. – Bongho Lee Jan 02 '18 at 10:12
  • Try ```GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); GC.WaitForPendingFinalizers();``` instead of just ```GC.Collect();``` and see if that changes your results. – pmcilreavy Jan 02 '18 at 10:15
  • @Saruman I just used bitmap just for example of the class using unmanaged resource. I used comercial math library class and it didn't support IDisposable. – Bongho Lee Jan 02 '18 at 10:15
  • `Bitmap` does implement `IDisposable` you should either call explicitly .Dispose() or encapsulate the bitmap inside a using. GC does not free resources still in use. – Cleptus Jan 02 '18 at 10:20
  • 1
    Ive got a feeling if you called gc.Collect on another thread it will suspend your work thread and it would behave more with what you'd expect – TheGeneral Jan 02 '18 at 10:28
  • @Saruman If I called GC.Collect on another thread, GC collected bitmap in all cases. – Bongho Lee Jan 02 '18 at 10:37
  • 1
    You're dealing with a race condition, nothing more, you're racing against the garbage collector. Why running it from a task changes it I don't know but the main problem is that you've got that is flawed, and the flaw is unpredictable. Fix the flaw and why it works in a task is a moot point. – Lasse V. Karlsen Jan 02 '18 at 10:42
  • 1
    Required reading on this subject: [Everybody thinks about garbage collection the wrong way](https://blogs.msdn.microsoft.com/oldnewthing/20100809-00/?p=13203) by Raymond Chen. – Daniel Pryden Jan 02 '18 at 10:44
  • Have you tried either putting the Bitmap creation in a using statement - or explicitly calling Dispose at the end of the loop? – PaulF Jan 02 '18 at 11:07
  • @PaulF Calling Dispose is not my option. Because I just used bitmap class for example. Actually I used other math library class which uses unmanaged resource and does not support IDisposable. – Bongho Lee Jan 02 '18 at 11:33
  • But you should be correctly disposing of instances of classes that do implement IDisposable. Why are you not using instances of the class you are using - why are you testing with a different class that works differently - you are not comparing similar objects so you cannot expect similar results. – PaulF Jan 02 '18 at 11:39
  • @PaulF If I used thirdy party comercial library class in example, other people could not test the code, so I changed to similar object which has unmanaged resource. I got same result using bitmap and math library class instance. – Bongho Lee Jan 02 '18 at 11:49
  • But they are NOT similar - one implements IDisposable & requires explicitly disposing & the other does not implement IDisposable. If the commercial package uses unmanaged resources - exactly how are they freed up? – PaulF Jan 02 '18 at 11:54
  • @PaulF Though they didn't implement IDisposable, I think they implemented the releasing code in destructor. So through calling GC.Collect, I could free the resource. – Bongho Lee Jan 02 '18 at 12:02
  • It may also be worth searching for the differences between Threads & Tasks : https://stackoverflow.com/questions/4130194/what-is-the-difference-between-task-and-thread – PaulF Jan 02 '18 at 12:02
  • 3
    Clearly the code snippet is completely fake and is incapable of reproducing this problem. I suspect the real issue is "Thread Count : 33", suggesting that a large number of threads create objects too fast, faster than the finalizer can clean them up again. Using Task.Run() does have a side-effect that could matter. It is throttled by the threadpool manager, it prevents too many threads from running at the same time. That this code requires help from the finalizer and does not implement IDisposable is quite ugly and should be fixed. – Hans Passant Jan 02 '18 at 12:47
  • Even this kind of simple unit test has 22 threads. [TestMethod] public void TestMethod1() { var count = Process.GetCurrentProcess().Threads.Count; } Even I changed Thread.Slee(10) to 1000 and Bitmap Width 1024 to 10240 for creating one bitmap object per second, GC didn't collect and the memory keeps growing. I sent you my test code. If you tested in x86 mode, you will see the error in 10 secs. – Bongho Lee Jan 03 '18 at 00:41
  • x86 will run out of memory a lot quicker obviously. – TheGeneral Jan 03 '18 at 07:50

1 Answers1

1

Its probably worth to have a read through this

Fundamentals of Garbage Collection

Before a garbage collection starts, all managed threads are suspended except for the thread that triggered the garbage collection.

The following illustration shows a thread that triggers a garbage collection and causes the other threads to be suspended.

enter image description here

Calling Collect from another thread may just help GC as it can suspend the work thread completely. Though i know this doesn't explain all your results

However the reason why WaitForPendingFinalizers() works

Is because it suspends the current thread until the thread that is processing the queue of finalizers has emptied that queue.

Also the internals of garbage collection has changed a lot in different versions of .Net, there are also different garbage collections you can use, i'e server vs work station

But perhaps your other threading objects are staying around too long and getting to the Long-Life Generation (Gen 1, or Gen 2), and garbage collection is not looking at them as often.

Survival and promotions. Objects that are not reclaimed in a garbage collection are known as survivors, and are promoted to the next generation. Objects that survive a generation 0 garbage collection are promoted to generation 1; objects that survive a generation 1 garbage collection are promoted to generation 2; and objects that survive a generation 2 garbage collection remain in generation 2.

TheGeneral
  • 79,002
  • 9
  • 103
  • 141
  • Thank you very much. I didn't know that all managed threads are suspended **except for** the thread that triggered the garbage collection. But I still have a question that why GC collects properly in Task.Run context. – Bongho Lee Jan 02 '18 at 10:51