析构函数从未被调用

20

我有一个名为 Class 的类,它在其构造函数中创建一个 Thread 。该线程运行一个 while(true)循环,从 NetStream 中读取非关键数据。线程将在析构函数中被中止:

~Class()
{
 _thread.Abort();
 _thread = null;
}

当程序想要结束使用Class类的实例 - ClassInstance时,它会调用:

ClassInstance = null;
GC.Collect;
我原以为这意味着~Class()会在此时自动调用,但实际上并不是这样的。
即使在执行了Application.Exit()并从Main()返回后,该线程仍然在运行。

17
C#类的析构函数不能保证被调用。请使用“Dispose 模式”来确定性地清理资源。 - Dustin Kingen
3
使用using()语句代替调用GC.Collect方法。一般建议不要主动调用GC.Collect方法。具体信息请参见:http://msdn.microsoft.com/zh-cn/library/yh598w02.aspx 和 https://dev59.com/RXRB5IYBdhLWcg3w4bKv。 - Oliver
将实例变量设置为 null 不等同于调用析构函数!实际上,它对对象实例几乎没有影响,只是少了一个引用。 - Thorsten Dittmar
1
就价值而言,最终器实际上会在几乎所有微不足道的情况下运行。 CriticalFinalizerObject 解决了一些松散的问题,但仍然存在某些情况(尽管非常有限),其中最终器可能无法完整运行。根据提供的信息,我认为可以安全地假定这些情况都没有发生。 - Brian Gideon
2
另外,调用 Thread.Abort 是一个可怕的想法。这就像是通过射击司机来停止一辆汽车。汽车会停下来,但无法预测在此过程中会造成什么样的损害。 - Jim Mischel
@JimMischel 很好的提示,但这只有在 _thread 有机会优雅地结束后才会执行,因为它正在监听的 Event 已经发出信号,并且终止程序已经等待完成。 - Edgar James luffternstat
4个回答

9
你代码中至关重要的部分并未包含;线程是如何启动以及它运行的方法。如果我要猜测,我会说你很可能是通过传递一个Class实例方法来启动线程的。所以基本上你的类实例仍然由线程的运行所引用。你试图在终结器中停止线程,但终结器永远不会运行,因为实例仍然被引用,导致了一个死循环的局面。
此外,你提到线程正在运行非关键代码,并以此为理由使用Thread.Abort。这真的不是一个充分的理由。很难控制ThreadAbortException注入线程的位置,因此可能会破坏你没有预料到的关键程序数据结构。
使用 TPL 中包含的新的协作取消机制。将while (true)循环更改为轮询CancellationToken。在实现IDisposable时,在Dispose方法中发出取消信号。不要包括终结器(C#术语中的析构函数)。终结器旨在用于清理非托管资源。由于你没有表明非托管资源正在运行,因此拥有终结器是毫无意义的。在实现IDisposable时不必包含终结器。事实上,当不真正需要时拥有终结器被认为是一种不好的做法。
public class Class : IDisposable
{
  private Task task;
  private CancellationTokenSource cts = new CancellationTokenSource();

  Class()
  {
    task = new Task(Run, cts.Token, TaskCreationOptions.LongRunning);
    task.Start();
  }

  public void Dispose()
  {
    cts.Cancel();
  }

  private void Run()
  {
    while (!cts.Token.IsCancellationRequested)
    {
      // Your stuff goes here.
    }
  }
}

我最近将GUI中使用的“BackgroundWorker”代码切换为使用“TPL”,并且印象非常深刻。 - SteveB

8
如果你实现了IDisposable并且释放了对象,那么Dispose方法中的代码将会运行,但不能保证析构函数也会被调用。
垃圾收集器认为这样做是浪费时间。因此,如果你想要可预测的释放资源,就需要使用IDisposable
请查看此线程

2

CLR维护所有正在运行的线程。您将会将您类的InstanceMethod作为ThreadStartParameterizedThreadStart委托传递给线程的构造函数。 Delegate将保存您传递的方法的MethodInfo和您类中的Instance,保存在Target属性中。

垃圾回收器收集不应具有任何Strong References的对象,但是您的实例仍然存在于ThreadDelegate中。因此,您的类仍具有Strong Reference,因此不符合垃圾回收条件。

为了证明我上面所说的话

public class Program
{
    [STAThread]
    static void Main(string[] args)
    {
        GcTest();

        Console.Read();
    }

    private static void GcTest()
    {
        Class cls = new Class();
        Thread.Sleep(10);
        cls = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

public class Class
{
    private Thread _thread;
    ~Class()
    {
        Console.WriteLine("~Class");
        _thread.Abort();
        _thread = null;
    }

    public Class()
    {
        _thread = new Thread(ThreadProc);
        _thread.Start();
    }

    private void ThreadProc()
    {
        while (true)
        {
            Thread.Sleep(10);
        }
    }
}

尝试上述代码。Destructor不会被调用。要使其工作,请将ThreadProc方法标记为static并重新运行,Destructor将被调用


2
略微偏题:您可以使用 Tasks 来运行函数,而无需担心处理问题。
这里有多个问题:
  • 将变量设置为 null 并不会删除任何内容,它只是删除了对实例的引用。
  • 垃圾回收器只有在检测到内存压力时才会定期运行,才会调用析构函数。
  • 垃圾回收器仅收集孤立的对象。孤立的对象意味着您对象指向的任何引用都是无效的。
您应该实现 IDisposable 接口,并在 Dispose 方法中调用任何清理代码。C# 和 VB 提供了 using 关键字,即使面临异常,也能更轻松地进行处理。
典型的 IDisposable 实现类似于以下内容:
class MyClass:IDisposable
{
    ClassB _otherClass;

    ...


    ~MyClass()
    {
         //Call Dispose from constructor
         Dispose(false);
    }

    public void Dispose()
    {
        //Call Dispose Explicitly
        Dispose(true);
        //Tell the GC not call our destructor, we already cleaned the object ourselves
        GC.SuppressFinalize(this);
    }

    protected virtual Dispose(bool disposing)
    {
        if (disposing)
        {
            //Clean up MANAGED resources here. These are guaranteed to be INvalid if 
            //Dispose gets called by the constructor

            //Clean this if it is an IDisposable
            _otherClass.Dispose();

           //Make sure to release our reference
            _otherClass=null;
        }
        //Clean UNMANAGED resources here
    }
}

您可以像这样使用您的类:
using(var myClass=new MyClass())
{
    ...
}

一旦using块终止,即使发生异常,也会调用Dispose()。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接