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

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

this SO answer 之后,我正在做:

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

我的目标是在关闭带有大量 images/PictureBox 控件的大型 WinForms 窗体后执行垃圾回收 运行,以确保内存中不再有图像。 (我相信我遵循 the instructions of Jon Skeet)。

我在后台线程中执行此操作以尝试让我的 UI 响应。

我的问题:

在后台线程中进行垃圾回收对我有什么好处吗?还是它真的让我的申请 slower/hang 变长了?

更新:这个答案中的推理似乎很合理,但下面 Hans Passant 的回答表明结论不成立。不要根据这个答案仓促下结论。


这是个好主意。所有 CLR GC 算法至少暂停每个线程一次,但暂停时间小于总 GC 时间。调用 GC.Collect 的时间与总 GC 时间一样长。它具有任何 GC 周期可能的最大延迟。这就是为什么最好不要在 UI 线程上调用它。

您的 UI 线程将在 GC 期间至少暂停一次,但不会暂停整个持续时间。这取决于 CLR 版本和 GC 设置会有多长时间和多少暂停。

总结:这减少了 UI暂停时间,但并不能完全避免。我建议这样做,因为不会造成任何伤害。

或者,处置所有非托管资源。这个问题似乎假设了一种不可能或太繁重的情况。

后台线程调用GC.Collect没有错。事实上,它根本没有任何区别。它只是一个线程;就是这样。

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

通过在后台线程中强制执行 GC,您可以使 UI 响应,但它会消耗与使用 UI 线程相同的 CPU 资源。如果保持 UI 响应是目标,是的,你可以。

也就是说,作为一般规则,您不会在生产代码中调用 GC.Collect。你为什么要这样做?如果关闭了一个大窗体,并且其中的所有对象都符合收集条件,则 Next GC 将对其进行收集。立即收集有什么好处?

同时通过 GC.Collect 强制垃圾收集会破坏 GC 的内部启发式算法。它将调整针对您的应用程序内存分配优化的集合的段的阈值限制 activity,通过调用 GC.Collect 您正在破坏它。

直接调用GC一般是bad的事情。 表单 class 实现了 Dispose Pattern 那么你为什么不使用它呢。

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

另一个重要的问题是 Windows 的工作站版本为任何拥有前台 window 的线程提供了更大的量程。换句话说,允许比后台线程更长时间地燃烧核心。使 Windows 对用户响应更快的简单 hack。

移动部件太多,最好测试一下你的理论,这样你就可以确定运行对工人进行集合实际上是你的事情领先于。测量 UI 线程暂停非常简单,您可以使用 Timer 来执行此操作。它的Tick事件在线程挂起时不能运行。启动一个新的 Winforms 项目,在窗体上放置一个 Timer,将其 Interval 设置为 1 并将 Enabled 设置为 True,添加一个 Label 并使用此代码来测量延迟:

    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。如果你得到的更少,那么你应该修理你的机器,而不是任何影响这个测试的东西。添加一个按钮来重置测量值:

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

玩这个,点击 button2 开始收集并注意 Label 中的值。打开复选框,使其 运行 在工人身上进行比较。使用 button1 重置两者之间的最大值。并修改分配代码,你可能想用位图做点什么,无论你做什么都需要这个 hack。

我看到的:在 UI 线程上执行收集时延迟约 220 毫秒,在工作线程上执行 运行 时延迟约 340 毫秒。显然,这根本 不是 的改进。从我的角度来看,你的理论已经死了。请自己尝试,我只有一个数据点。请注意,它在 Windows 的服务器版本或 .config 文件中的 <gcServer=true> 看起来会非常不同。其他你可以玩的东西。