【问题标题】:Isn't C# finalizer mechanics fundamentally flawed even for the case it was designed for?C# 终结器机制是否存在根本缺陷,即使对于它的设计目的而言也是如此?
【发布时间】:2019-11-10 06:42:03
【问题描述】:

我的理解是,C# 中存在终结器的主要原因是提供一个“安全网”,以防您有一个具有非托管资源的类而您忘记处置它。

让我们看看这个小 C# 方法:

public int GetValue()
{
    var calculator = new Calculator();
    return calculator.GetValue();
}

Calculator 类是一个包含非托管资源的 C++/CLI 类,应该清楚地处理它,最好是在 using 语句的帮助下。但是,嘿,我忘了这样做!

Calculator 类如下所示:

// header Calculator.h
public ref class Calculator
{
    public:
        Calculator();
        ~Calculator();
        int GetValue();
    protected:
        !Calculator();
    private:
        CalculatorNative* _calculatorNative;
};

// implementation Calculator.cpp
Calculator::Calculator()
{
    _calculatorNative = new CalculatorNative();
}

int Calculator::GetValue()
{
    return _calculatorNative->GetValue();
}

Calculator::~Calculator()
{
    this->!Calculator();
}

Calculator::!Calculator()
{
    delete _calculatorNative;
    _calculatorNative = nullptr;
}

CalculatorNative 类是一个标准的 C++ 类,它在其GetValue 方法中进行了一些耗时的计算(我认为这里的细节并不重要)。

现在,我忘记为其调用 Dispose 的 calculator 对象,在 CalculatorNative::GetValue()Calculator::GetValue() 调用后,就可以进行垃圾回收了。

所以想象CalculatorNative::GetValue() 方法正处于其繁重计算的中间,现在 GC 开始四处嗅探并发现可以收集calculator(它的this 再也不会被使用)。但是它有一个终结器,因此它将终结器添加到终结器队列中。 CalculatorNative::GetValue() 方法仍在计算某些东西,现在 Calculator::!Calculator()(终结器)在不同的线程上运行,天啊,它删除了 _calculatorNative,即使它仍在做某事!当然,CalculatorNative::GetValue() 方法会严重崩溃。

现在的问题:

  • 这真的是终结器的使用方式吗?
  • 是否可以为我刚刚描述的如此简单的场景编写正确的终结器?

编辑: 现在我看到这个标题可能听起来有点冒犯。 我很抱歉。这绝对不是我的本意。 我只是花了太多时间调试这个问题:-(

【问题讨论】:

  • “C# 中存在的终结器是为了提供一个“安全网”,以防你有一个具有非托管资源的类而你忘记了释放它”——不,从不。事实上,有可能 GC 将根本不运行并且不会执行任何终结器。
  • 如果没有对对象的引用,它怎么会在任何东西的中间呢?它是如何在没有完成它想要做的事情的情况下从调用中返回的?如果它确实产生了一个线程来做某事,那么这就是一个设计问题
  • 这是一个很难掌握的话题,但处理这个问题的正确方法是确保在调用本机代码的过程中不会发生 GC。例如,你可以通过 int result = _calculatorNative.GetValue(); GC.KeepAlive(this); return result; 来解决这个问题(我不确定你用什么语言写你的课,因为你使用的是 ->:: 所以你必须适应你的语言)
  • 基本上,你是在引用this 来获得_calculatorNative,然后调用GetValue() 方法,但如果这是对对象的最后一个 引用由this 引用,现在可以根据优化规则进行收集。您需要确保您的 this 对象保持活动状态,直到您的本机代码返回。
  • 所以,是的,它可以正常工作,但正如您所发现的那样,存在很多缺陷。

标签: c#


【解决方案1】:

在调用本机代码时,您需要确保您的对象不符合收集条件。

一种方法是这样重写GetValue

public int GetValue()
{
    int result = _calculatorNative.GetValue();
    GC.KeepAlive(this);
    return result;
}

这确保了 GC 无法拆除您的对象并使其变得脆弱,直到对您的本机对象的调用返回之后,即使没有更多对该对象的实时引用。


由于问题上的 cmets 似乎表明肯定有比您的示例代码更多的事情发生,我将在这里分解问题所在。

在您的第一种方法中:

public int GetValue()
{
    var calculator = new Calculator();
    return calculator.GetValue();
}

在 call 指令之后不再使用 calculator 变量,这意味着一旦执行转换到 GetValuecalculator 被认为是死的,不再用于保持 Calculator 对象处于活动状态.

但是,我们只是将 GetValue 方法的引用作为 this 隐式参数传递,所以让我们看看该方法(我将其重写为 C# 语法只是因为):

int GetValue()
{
    return _calculatorNative.GetValue();
}

你可以眯着眼睛看这个方法,好像它是这样写的:

int GetValue()
{
    var localVariable = this._calculatorNative;
    return localVariable.GetValue();
}

在您取消引用this 以到达_calculatorNative 之后,GetValue 不再使用this,这意味着除非有其他引用保持对象处于活动状态,没有对对象的更多引用

由于我们是从唯一引用该对象的其他方法调用的,并且该对象也不再具有对该对象的实时引用,因此对于所有意图和目的,该对象现在都符合收集条件,甚至如果我们仍在调用原生 GetValue() 方法

因此,如果发生 GC,并且调用了终结器,这导致问题,因为我们仍在执行的方法是在我们现在删除的内存块中的对象上(根据代码在终结器中)。

解决方案是“简单的”,因为您可以轻松地在此示例中添加一些代码以确保此问题消失,但正如 OP 在此答案下方的评论中所写,对于更复杂的情况,它可能会得到真的很快就失控了。不过,这是设计的,您需要了解它并相应地进行设计。

如上所示,修复方法是确保对象在调用本机方法完成后才符合收集条件,这可以通过不透明的GC.KeepAlive(this) 方法调用来完成。这是一个无操作方法调用,但它已被标记为编译器无法对其进行优化,因此 在本机调用之后使用 this 变量,因此该对象符合收集条件。

但是,在方法返回之前,在对GC.KeepAlive 的调用完成之后但在我们返回之前,它可以被收集。与大多数其他情况一样,这应该不再是问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-03-06
    • 2019-04-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多