即使是针对C#终结器设计的,它是否也从根本上没有缺陷?

时间:2019-06-28 09:49:00

标签: c#

我的理解是,在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()方法会严重崩溃。

现在是问题:

  • 这真的是要使用终结器的方式吗?
  • 即使是我刚才描述的这种简单情况,也可以编写正确的终结器吗?

编辑: 现在,我看到标题听起来有些冒犯。 对此我感到抱歉。这绝对不是我的意图。 我只是花了很多时间来调试这个问题:-(

1 个答案:

答案 0 :(得分:3)

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

做到这一点的一种方法是这样重写GetValue

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

这确保即使在不再有对该对象的实时引用的情况下,GC也无法拆除您的对象并使其可恢复直到返回对本机对象的调用。


由于对问题的评论似乎表明,除了您的示例代码之外,还必须进行更多的工作,因此我将在这里分解出问题所在。

第一种方法:

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

在调用指令之后,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的调用完成之后但在我们返回之前进行收集。与大多数其他情况一样,这不再是问题。