在后台线程中运行GC.Collect是否可以?

18

参照这个SO答案,我正在执行:

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

我的目标是在关闭有大量图片/PictureBox控件的大型WinForms表单后运行垃圾回收,以确保我没有任何图像留在内存中。(我相信我遵循了Jon Skeet的指示)。

我在后台线程中执行此操作,以尝试使UI响应快速。

我的问题:

在后台线程中进行垃圾回收是否会带来任何好处?还是它实际上会使我的应用程序变慢/挂起更长时间?


3
在后台线程中完成这件事,你期望看到什么行为上的不同? - Remus Rusanu
2
@RemusRusanu 如果没有在后台线程中调用,我会期望WaitForPendingFinalizers 阻塞我的UI线程。我的假设是错误的吗? - Uwe Keim
1
@Uwe 非常有趣 :-) - xanatos
1
好的。你仍然会在“Collect()”上阻塞,但WFPF不会阻塞主UI。 - Remus Rusanu
2
如果你在执行此操作时仍在运行UI,则会将某些数据保存在内存中的时间比实际需要的时间更长。为什么不让GC自动清除您的图像呢? - Dan Puzey
1
你应该调查一下为什么释放窗体不起作用。 - Random832
4个回答

26

当您这样做时,您正在放弃在后台执行垃圾收集的选项。换句话说,您的UI线程将被挂起,无论您是否从工作线程中执行此操作。唯一可能领先的方法是当GC.WaitForPendingFinalizers()需要大量时间时。实际上,这不是您应该等待的东西,没有任何意义,如果它花费的时间超过一眨眼那么您正在隐藏代码中相当严重的错误。

另一个重要的问题是,Windows工作站版本会给予拥有前景窗口的任何线程更长的时间片。换句话说,它可以比后台线程更长时间地使用核心资源。这是使Windows对用户更具响应性的一个简单技巧。

由于涉及的因素太多,最好的方法是测试您的理论,以确保在工作线程上运行垃圾回收确实能够提高性能。测量UI线程的挂起时间非常简单,您可以使用计时器来完成。其Tick事件无法在线程挂起时运行。开始一个新的Winforms项目,在窗体上放置一个计时器,将其间隔设置为1并启用,添加一个标签并使用此代码来测量延迟:

    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;
    }

运行程序,你应该在标签中看到数字16。如果你得到的数字小于16,那么你需要修理你的机器,不是其他任何影响测试的原因。添加一个按钮来重置测量:

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

添加一个复选框和另一个按钮。它将执行实际的收集:

    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();
                }));
            }
        });
    }

玩一下这个,点击按钮2开始收集,并注意标签中的数值。打开复选框使其在worker上运行并比较。使用按钮1在之间重置最大值。修改分配代码,你可能想要对位图做些什么,无论你做什么都需要这个hack。

我的观察是:在UI线程上执行收集时延迟约为220毫秒,在worker上运行时延迟约为340毫秒。显然,这根本没有改进。就我所知,你的理论已经不能成立。请自己尝试一下,我只有一个数据点。请注意,在Windows服务器版本或.config文件中使用<gcServer=true>将会显示非常不同。还有其他东西可以玩玩。


9
更新:该回答的推理似乎非常合理,但下面由Hans Passant提供的答案表明结论不成立。请不要根据此答案得出结论。
这是个好主意。所有CLR GC算法都至少会一次暂停每个线程,但暂停时间比总GC时间短。调用GC.Collect的时间与总GC时间一样长。对于任何GC循环来说,它具有最大的延迟时间。所以在UI线程上不调用它是个好主意。
在GC过程中,您的UI线程将至少被暂停一次,但不会持续整个时间。它取决于CLR版本和GC设置,会有多长时间和多少暂停。
总结:这会减少UI暂停时间,但并不能完全避免它。我建议这样做,因为没有造成任何伤害。
或者,处理所有未受管资源。这个问题似乎假设有一个情况,即这不可能或太麻烦。

4
直接调用GC通常是一件糟糕的事情。 Forms类实现了Dispose模式,为什么不使用它呢。

3
谢谢,我想我已经意识到了。我的问题更像是"如果我必须这么做,那么怎么做呢?" - Uwe Keim
+1:这是很好的建议。内存不会立即释放并不是问题(只要有压力释放它并且不会造成任何损害,它就会被释放)。手动GC有益的情况非常罕见,而在具有UI的应用程序中更加罕见。 - Dan Puzey
5
释放对象并不会释放它们所使用的托管内存。Dispose 方法将释放非托管资源(其中可能包括非托管内存)。垃圾回收器负责管理托管内存,并在此过程中处理不再使用的未释放对象。 - Martin Liversage

2

在后台线程中调用GC.Collect是没有问题的。实际上,这并不会有任何区别。它只是一个线程而已。

但我不确定你为什么要调用两次GC.Collect。据我所知,GC.Collect后跟GC.WaitForPendingFinalizers就足够了。

通过在后台线程中强制进行GC,可以使UI响应性更高,但它将消耗与使用UI线程相同的CPU资源。如果保持UI响应性是目标,那么可以这样做。

话虽如此,通常情况下不会在生产代码中调用GC.Collect。为什么要这样做呢?如果关闭一个大型窗体并且其中所有对象都符合收集条件,则下一个GC将对其进行回收。立即进行回收有什么好处呢?

此外,通过GC.Collect强制垃圾回收会破坏GC的内部启发式算法。它会通过调用GC.Collect来调整段的阈值限制,以优化您的应用程序内存分配活动的收集。因此,强制执行垃圾回收可能会破坏这种优化。


2
.NET 会在程序空闲时调用 GC.Collect 吗?我正在看“如果关闭一个大型窗体并且其中的所有对象都可以进行回收”的句子。 - xanatos
1
这取决于程序的内存分配和系统资源的可用性。如果系统没有内存压力,且您的应用程序没有分配任何内存,则不会出现该问题。我认为这不是个问题,因为您那里没有任何内存压力。 - Sriram Sakthivel
2
这是一种自我中心和自负的推理。你的程序并不是系统上唯一可能运行的程序。我认为你应该尝试“友好相处”,而不是“把系统当作只属于我的”。 - xanatos
1
@xanatos 我相信尽可能地节约资源。但是立即强制 GC.Collect 会占用 CPU 周期。为什么要浪费不必要的资源呢?可能同一台机器上的另一个进程需要大量 CPU,但你正在为了没有明显原因而回收内存而占用它的时间(这最终会发生)。 - Sriram Sakthivel
1
显然需要进行平衡(例如,在for循环中不能执行GC.Collect),但是几百MB的内存占用数分钟/小时与大约0.1秒的CPU时间通常很容易平衡,而且CPU时间比内存更容易被操作系统公平地共享(不幸的是,即使CPU时间也并不总是完全公平地被操作系统共享,但这是另一个问题)。 - xanatos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接