9

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

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

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

验证过程是一个相当耗时的操作,所以我将它交给一个任务。我遇到的问题是,最初我没有调用 GC.SuppressFinalize(calibrationValidator) 并且当应用程序从 Release 版本运行时,输出参数 validationResultsUsingRecomputedExtrinsics 将为空。如果我从 Debug 版本运行应用程序(无论是否附加了 Debugger),那么 validationResultsUsingRecomputedExtrinsics 将包含有效数据。

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

添加对 GC.SuppressFinalize(calibrationValidator) 的调用如何/为什么解决此问题?

我知道,如果没有对供应商库内部的深入了解,可能无法确定,但任何洞察力都会有所帮助。

该应用程序使用 VS2012 编译,面向 .NET 4.0。该供应商库要求在 app.config 中指定 useLegacyV2RuntimeActivationPolicy="true" 选项。

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

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

因为变量是在本地范围内创建的,而该函数在后台线程中运行,所以垃圾收集运行在主线程中,看起来垃圾收集在处理多线程情况时不够聪明。有时,它只是过早地释放它(验证器的内部执行尚未完成,仍然需要这个变量)。

4

3 回答 3

16

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

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

最终的正确解决方法是确保抖动标记在调用之后正在使用的对象,以便垃圾收集器不会收集它。您可以GC.KeepAlive(calibrationValidator)Execute()通话后添加。

于 2013-03-22T03:24:42.577 回答
3

当谈到理解C# 中的IDisposableGC.SuppressFinalize和终结器时,我认为没有比以下文章更好的解释了。

DG 更新:处置、最终确定和资源管理

好的!这里是:修订后的“处置、完成和资源管理”设计指南条目。我之前在这里这里提到过这项工作。在大约 25 页的打印页上,这不是我认为的小更新。我花了比预期更长的时间,但我对结果很满意。我开始与 HSutter、BrianGru、CBrumme、Jeff Richter 和其他几个人一起工作,并收到了很多关于它的反馈……很好玩。

这个问题的关键概念:

明显GC.SuppressFinalize()应该只被称为this文章甚至没有直接提到这一点。然而,它确实提到了包装可终结对象以将它们与公共 API 隔离的做法,以确保外部代码无法调用GC.SuppressFinalize()这些资源(请参阅以下引用)。设计原始问题中描述的库的人不了解 .NET 中的最终确定的工作方式。

引用自博客文章:

即使在没有上述罕见情况之一的情况下,具有可公开访问引用的可终结对象也可能被任意不受信任的调用者抑制。具体来说,他们可以对您调用 GC.SuppressFinalize 并完全阻止终结,包括关键终结。解决此问题的一个很好的缓解策略是将关键资源包装在具有终结器的非公共实例中。只要您不将此泄漏给调用者,他们将无法抑制最终确定。如果您迁移到在您的课程中使用 SafeHandle 并且从不在您的课程之外公开它,您可以保证您的资源的最终确定(使用上面提到的警告并假设正确的 SafeHandle 实现)。

于 2013-03-22T01:52:39.273 回答
1

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

考虑以下程序:

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将被垃圾收集并最终确定(至少在发布模式下)。终结器将Inner对象的字段清空,这意味着GetValue()将返回null

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

(我说这段代码大部分是单线程的,其实finalizer会跑在另一个线程上,但是因为调用了WaitForPendingFinalizers(),就好像跑在主线程上一样。)

于 2013-03-22T18:50:08.270 回答