Is it OK to run GC.Collect in a background thread?

2019-03-18 02:59发布

问题:

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?

回答1:

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.



回答2:

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.



回答3:

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



回答4:

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.