18

Following this SO answer, I'm doing:

ThreadPool.QueueUserWorkItem(
    delegate
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    });

My goal is to do a garbage collection run after I close a large WinForms form with lots of images/PictureBox controls to ensure I have no images in memory anymore. (I do believe I follow the instructions of Jon Skeet).

I'm doing it in a background thread in order to try to have my UI responsive.

My question:

Does it bring me any benefits to do the garbage collection in a background thread? Or does it actually make my application slower/hang longer?

Community
  • 1
  • 1
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
  • 3
    What difference in behavior you expect from this being done in a *background* thread? – Remus Rusanu Feb 27 '15 at 09:38
  • 2
    @RemusRusanu I would expect [`WaitForPendingFinalizers`](https://msdn.microsoft.com/en-us/library/system.gc.waitforpendingfinalizers) to be blocking my UI thread if _not_ being called in a background thread. Is my assumption wrong? – Uwe Keim Feb 27 '15 at 09:39
  • 1
    @Uwe Very very interesting :-) – xanatos Feb 27 '15 at 09:44
  • 1
    OK. You'll still block on `Collect()`, but WFPF will not block main UI. – Remus Rusanu Feb 27 '15 at 09:46
  • 2
    If you're still running a UI at the point where you do this, you're going to keep certain data in memory a lot longer than you need to. Why can't you just let the GC clear up your images automatically? – Dan Puzey Feb 27 '15 at 09:49
  • 1
    You should investigate why disposing the form doesn't work. – Random832 Feb 27 '15 at 19:51

4 Answers4

26

You are throwing away the option to have garbage collection performed on the background when you do this. Or in other words, your UI thread is going to get suspended anyway, regardless if you do this from a worker thread. The only possible way to be ahead is when GC.WaitForPendingFinalizers() is taking a substantial amount of time. It is not actually something you should ever be waiting for, there is no point, and if it takes more than the blink of an eye then you are hiding pretty serious bugs in your code.

Another significant wrinkle is that the workstation version of Windows gives any thread that owns the foreground window a larger quantum. In other words, it is allowed to burn core longer than a background thread. A simple hack to make Windows more responsive to the user.

Too many moving parts, it is really rather best to test your theory so you can be sure that running a collection on a worker is actually something you are ahead with. Measuring UI thread suspensions is pretty simple, you can use a Timer to do this. Its Tick event cannot run when the thread is suspended. Start a new Winforms project, drop a Timer on the form, set its Interval to 1 and Enabled to True, add a Label and use this code to measure delays:

    int prevtick = 0;
    int maxtick = -1;

    private void timer1_Tick(object sender, EventArgs e) {
        int tick = Environment.TickCount;
        if (prevtick > 0) {
            int thistick = tick - prevtick;
            if (thistick > maxtick) {
                maxtick = thistick;
                label1.Text = maxtick.ToString();
            }
        }
        prevtick = tick;
    }

Run your program, you should be seeing 16 in the label. If you get less then you ought to get your machine fixed, not otherwise anything that affects this test. Add a button to reset the measurement:

    private void button1_Click(object sender, EventArgs e) {
        maxtick = -1;
    }

Add a checkbox and another button. We'll have it perform the actual collection:

    private void button2_Click(object sender, EventArgs e) {
        var useworker = checkBox1.Checked;
        System.Threading.ThreadPool.QueueUserWorkItem((_) => {
            var lst = new List<object>();
            for (int ix = 0; ix < 500 * 1024 * 1024 / (IntPtr.Size * 3); ++ix) {
                lst.Add(new object());
            }
            lst.Clear();
            if (useworker) {
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }
            else {
                this.BeginInvoke(new Action(() => {
                    GC.Collect();
                    GC.WaitForPendingFinalizers();
                }));
            }
        });
    }

Play with this, hit button2 to start the collection and pay attention to the value in the Label. Turn on the checkbox so it runs on the worker and compare. Use button1 to reset the maximum in between. And modify the allocation code, you probably want to do something with bitmaps, whatever you do to require this hack.

What I see: ~220 msec delay when performing the collection on the UI thread, ~340 msec delay when running on the worker. Clearly, this is not an improvement at all. From where I sit, your theory is dead in the water. Please try this yourself, I've got only one datapoint. Do beware that it is going to look very different on a server version of Windows or with <gcServer=true> in the .config file. Something else you can play with.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
9

Update: The reasoning in this answer seems pretty sound but the answer by Hans Passant below shows that the conclusion does not hold. Don't jump to conclusions based on this answer.


This is a good idea. All CLR GC algorithms pause each thread at least once but the pauses are smaller than the total GC time. The call to GC.Collect takes as long as the total GC time takes. It has the maximum latency possible for any GC cycle. That's why it is a good idea to not call it on the UI thread.

Your UI thread will be paused during the GC at least once but not for the whole duration. It depends on the CLR version and GC settings how long and how many pauses there will be.

Summary: This reduces UI pause time but does not entirely avoid it. I recommend doing this since there is no harm being done.

Alternatively, dispose all unmanaged resources. This question seems to a assume a situation where that is not possible or too onerous.

usr
  • 168,620
  • 35
  • 240
  • 369
4

Calling the GC directly is generally a bad thing. The Forms class implements the Dispose Pattern so why don't you use it.

darkArk
  • 144
  • 1
  • 11
  • 3
    Thanks, I do think I'm aware of that. My question is more like "If I have to do it, then how?". – Uwe Keim Feb 27 '15 at 09:45
  • +1: this is good advice. Memory not being immediately released is not an issue (it will be released as soon as there's pressure to do so and isn't causing any harm). It's extremely rare that manual GC is beneficial, and that's even more rare in an app with a UI on screen. – Dan Puzey Feb 27 '15 at 09:51
  • 5
    Disposing object does not release the managed memory they use. Dispose will release unmanaged resources (which might include unmanaged memory). The garbage collector takes care of the managed memory and in the process also dispose undisposed objects that are no longer in use. – Martin Liversage Feb 27 '15 at 10:07
2

There is nothing wrong in calling GC.Collect in BackGround thread. In fact it doesn't makes any difference at all. It is just a thread; that's it.

But I'm not sure why are you calling GC.Collect twice. AFAIK GC.Collect followed by GC.WaitForPendingFinalizers is sufficient.

By forcing GC in background thread you can make the UI responsive, but it will consume the same CPU resources as it would if you used the UI thread. If keeping the UI responsive is the goal, yes you can.

That said, as a general rule you don't call GC.Collect in your production code. Why would you do it? If a large form is closed and all objects inside it are eligible for collection, Next GC will collect it. What benefit you get by collecting it immediately?

Also forcing Garbage collection via GC.Collect spoils GC's internal heuristics. It will adjust the threshold limit of the segments for the collection optimized for your application memory allocation activity, by calling GC.Collect you're spoiling it.

Sriram Sakthivel
  • 72,067
  • 7
  • 111
  • 189
  • 2
    Is there any guarantee that the .NET will call GC.Collect on an idle program? I'm looking at the "If a large form is closed and all objects inside it are eligible for collection" sentence – xanatos Feb 27 '15 at 09:45
  • 1
    @xanatos It depends on memory allocation of the program and the resource availability of the system. If system isn't at memory pressure and your application isn't allocating any memory, It won't. I believe that's not a problem because you're not having any memory pressure there. – Sriram Sakthivel Feb 27 '15 at 09:49
  • 2
    this is an egoistical and egotistical reasoning. Your program isn't the only program that could be running on a system. I do think that you should try to "play nice" and not to "run the system as if it's only mine" – xanatos Feb 27 '15 at 09:56
  • 1
    @xanatos I do believe in consuming as less resources as possible. But Forcing a `GC.Collect` immediately eats the CPU cycles. Why would you want to waste the resource unnecessarily? It could be that another process in the same machine is in needy of CPU heavily, but you're taking its turn for reclaiming memory for no apparent reason(which will happen eventually). – Sriram Sakthivel Feb 27 '15 at 10:00
  • 1
    Clearly there is a balancing that needs to be done (you can't do a GC.Collect in a for cycle for example), but potentially hundred of mb for minutes/hours vs probably 0.1sec of cpu time is normally an easy balancing, and CPU time is more easily fairly shared by the OS than memory (sadly even CPU time isn't always completely fairly shared by the OS, but that is another problem) – xanatos Feb 27 '15 at 10:05