GC.SuppressFinalize()的这种用法感觉不太对

本文关键字:感觉 用法 SuppressFinalize GC | 更新日期: 2023-09-27 18:05:07

我在使用供应商库时遇到了一些问题,其中偶尔由库计算的实体在应该始终包含有效数据时将为空。

功能代码(在与供应商调试问题后)大致如下:

    Task.Factory.StartNew(() => ValidateCalibration(pelRectRaw2Ds, crspFeatures, Calibration.Raw2DFromPhys3Ds));
    .....
    private void ValidateCalibration(List<Rectangle> pelRectRaw2Ds, List<List<3DCrspFeaturesCollection>> crspFeatures, List<3DCameraCalibration> getRaw2DFromPhys3Ds)
    {
        var calibrationValidator = new 3DCameraCalibrationValidator();
        // This is required according to vendor otherwise validationResultsUsingRecomputedExtrinsics is occasionally null after preforming the validation
        GC.SuppressFinalize(calibrationValidator);
        3DCameraCalibrationValidationResult validationResultUsingOriginalCalibrations;
        3DCameraCalibrationValidationResult validationResultsUsingRecomputedExtrinsics;
        calibrationValidator.Execute(pelRectRaw2Ds, crspFeatures, getRaw2DFromPhys3Ds, out validationResultUsingOriginalCalibrations, out validationResultsUsingRecomputedExtrinsics);
        Calibration.CalibrationValidations.Add(new CalibrationValidation
            {
                Timestamp = DateTime.Now,
                UserName = Globals.InspectionSystemObject.CurrentUserName,
                ValidationResultUsingOriginalCalibrations = validationResultUsingOriginalCalibrations,
                ValidationResultsUsingRecomputedExtrinsics = validationResultsUsingRecomputedExtrinsics
            });
    }

验证过程是一个相当耗时的操作,所以我把它交给Task。我遇到的问题是,最初我没有调用GC.SuppressFinalize(calibrationValidator),当应用程序从发布版本运行时,输出参数validationResultsUsingRecomputedExtrinsics将为空。如果我从Debug构建中运行应用程序(附带或不附带调试器),那么validationResultsUsingRecomputedExtrinsics将包含有效数据。

我不完全理解GC.SuppressFinalize()在这种情况下做了什么,或者它是如何解决这个问题的。我所能找到的关于GC.SuppressFinalize()的一切都是在实现IDisposable时使用的。我在"标准"代码中找不到任何用法。

如何/为什么增加调用GC.SuppressFinalize(calibrationValidator)解决这个问题?

我明白,如果不深入了解供应商库的内部,可能不可能确切地知道,但任何洞察力都会有所帮助。

该应用程序是用VS2012编译的,目标是。net 4.0。该供应商库要求在app.config中指定useLegacyV2RuntimeActivationPolicy="true"选项。

这是我从供应商那里得到的理由:

SuppressFinalize命令确保垃圾收集器不会"提前"清理某些内容。似乎出于某种原因,您的应用程序有时会让垃圾收集器变得有点热心,并在您真正完成之前清理对象;它几乎肯定是与作用域相关的,并且可能是由于多线程导致的对校准器作用域的混淆。以下是我从工程部得到的回复。

因为变量是在局部范围内创建的,而该函数在后台线程中运行,垃圾收集在主线程中运行,似乎垃圾收集在处理多线程情况时不够智能。有时,它只是过早地释放它(validator的内部执行尚未完成,仍然需要这个变量)。

GC.SuppressFinalize()的这种用法感觉不太对

这很可能是一种解决过早垃圾收集问题的方法。在非托管代码中并不少见,在相机应用程序中很常见。这不是一个健康的hack,很有可能会导致资源泄漏,因为终结器没有执行。非托管代码的包装器几乎总是在终结器中有一些事情要做,它们通常需要释放非托管内存。

问题在于,当非托管代码运行时,calibrationValidator对象可以被垃圾收集。在您的程序中有另一个线程使这成为可能,因为其他线程可以分配对象并触发GC。代码的所有者在测试时很容易忽略这一点,或者是在使用多个线程时从未测试过它,或者只是没有足够幸运地在错误的时间触发GC。

正确的修复方法是确保抖动在调用之后标记出正在使用的对象,这样垃圾收集器就不会收集它。通过在Execute()调用后添加GC.KeepAlive(calibrationValidator)来实现。

要理解c#中的IDisposableGC.SuppressFinalize和终结器,我认为没有比下面这篇文章更好的解释了。

DG Update: Dispose, final, and Resource Management

好吧!这里是:修订后的"处置、终结和资源管理"设计指南条目。我之前在这里和这里提到过这项工作。在大约25个打印页面,这不是我认为是一个小的更新。花的时间比预期的要长,但我对结果很满意。我与HSutter, BrianGru, CBrumme, Jeff Richter以及其他一些人合作并收到了大量反馈。很有趣的。

这个问题的关键概念:

很明显 GC.SuppressFinalize()应该只在this上调用,所以文章甚至没有直接提到。然而,它确实提到了包装可终结对象的实践,以将它们与公共API隔离开来,以确保外部代码无法在这些资源上调用GC.SuppressFinalize()(参见以下引用)。无论谁设计了最初问题中描述的库,他都不了解。net中最终化的工作方式。

引自博客文章:

即使没有上面提到的罕见情况之一,具有公共可访问引用的可终结对象也可能被任何任意不受信任的调用者抑制其终结。具体来说,它们可以调用GC。在你身上抑制finalize并防止最终化发生,包括关键的最终化。处理此问题的一个良好缓解策略是将关键资源包装在具有终结器的非公共实例中。只要您不将此泄露给调用者,它们将无法抑制终结。如果你迁移到在类中使用SafeHandle,并且从不在类外公开它,你就可以保证资源的终结(使用上面提到的注意事项,并假设正确的SafeHandle实现)。

有一些提到多线程或本机代码是导致此问题的原因。但同样的事情也可能发生在纯托管的单线程程序中。

考虑以下程序:

using System;
class Program
{
    private static void Main()
    {
        var outer = new Outer();
        Console.WriteLine(outer.GetValue() == null);
    }
}
class Outer
{
    private Inner m_inner = new Inner();
    public object GetValue()
    {
        return m_inner.GetValue();
    }
    ~Outer()
    {
        m_inner.Dispose();
    }
}
class Inner
{
    private object m_value = new object();
    public object GetValue()
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        return m_value;
    }
    public void Dispose()
    {
        m_value = null;
    }
}

在这里,当outer.GetValue()被调用时,outer将被垃圾收集并最终确定(至少在Release模式下)。结束器将取消Inner对象的字段,这意味着GetValue()将返回null

在实际代码中,您很可能不会在那里调用GC。相反,您应该创建一些托管对象,它(不确定地)导致垃圾收集器运行。

(我说过这段代码主要是单线程的。实际上,终结器将在另一个线程上运行,但是由于调用了WaitForPendingFinalizers(),它几乎就像在主线程上运行一样。