使用Dispose()还是终结器来清理托管线程?

4
假设我有一个 C++0x 中的消息泵类,如下所示(请注意,SynchronizedQueue 是一个函数队列 <void()>,当您在队列上调用 receive() 并且它为空时,它会阻塞调用线程,直到有一个项目返回):
class MessagePump
{
 private:
    bool done_;
    Thread* thread_;
    SynchronizedQueue queue_;

    void Run()
    {
        while (!done)
        {
            function<void()> msg = queue_.receive();
            msg();
        }
    }
 public:
    MessagePump(): 
        done_(false)
    {
        thread_ = new thread ([=] { this->Run(); } ) );
    }

    ~MessagePump()
    {
        Send( [&]{ done = true; } );
        thread_->join();
    }

    void Send (function<void()> msg)
    {
        queue_.send(msg);
    }
};

我已将这个类转换为C#,但是对于析构函数中的代码我有一个问题。根据IDisposable模式,我应该只提供一个Dispose()方法以释放托管和非托管资源。
我应该将C++析构函数代码放在以下哪个位置:
1.自定义CleanUp()方法,客户端需要在应用程序退出时调用它?如果客户端忘记了怎么办?
2.IDisposable的Dispose()方法中,以便客户端也可以调用它?但是,如果客户端忘记了呢?
3.在C#终结器方法中,以便它总是执行?我读到如果你没有任何非托管资源,就不应该包含终结器方法,因为会影响性能。
4.不放在任何地方?只需忽略标记done_标志并让GC自然处理,因为Thread对象是托管资源?线程会被强制中止吗?
此外,我还发现如果我不将构造函数中创建的消息泵线程标记为后台线程,我的MessagePump对象永远不会被GC,当应用程序退出时会卡住。这是什么原因?
2个回答

2
在高层次上,我建议使用.NET线程池(System.Threading.ThreadPool)来排队和执行多个工作项,因为它是为此而设计的(假设允许异步执行工作项)。具体来说,请查看QueueUserWorkItem方法。
不过,回答你的问题:

我应该把C++析构函数代码放在哪里:

客户端需要在应用程序退出时调用自定义的CleanUp()方法吗?如果客户端忘记了怎么办?

还是放在IDisposable的Dispose()方法中,以便客户端也可以调用它?但是,如果客户端忘记了怎么办?

始终优先使用实现IDisposable而不是自定义CleanUp方法(在BCL中,一些Stream类具有一个名为Close的方法,它实际上只是Dispose的别名)。使用IDisposable模式可以在C#中进行确定性清理。客户端忘记调用Dispose总是一个问题,但这通常可以通过静态分析工具(例如FxCop)检测到。

在C#终结器方法内部,所以它将始终执行?我读到如果您没有任何非托管资源,则不应包括终结器方法,因为它会影响性能。

不能保证终结器方法会执行(请参阅this文章),因此正确的程序不能假设它们会执行。性能在这里不是问题。我猜你最多只有几个MessagePump对象,因此拥有终结器的成本微不足道。

无处?只需忽略标记done_标志,让GC自然处理,因为Thread对象是托管资源?这种方式会强制终止线程吗?
该线程由CLR管理,将被正确清理。如果线程从其入口点(这里是Run)返回,则不会被中止,它只会干净地退出。不过,这段代码仍然需要去某个地方,所以我会通过IDisposable提供显式清理。
我还发现,如果我不将构造函数中创建的消息泵线程标记为后台线程,则我的MessagePump对象永远不会被GC,当应用程序退出时就会挂起。原因是什么?
.NET应用程序运行直到所有前台(非后台)线程终止。因此,如果您不将MessagePump线程标记为后台线程,则它将在运行时保持应用程序活动状态。如果某个对象仍然引用您的MessagePump,则MessagePump将永远不会被GC或最终化。不过,请参考上面提到的文章,您不能假设最终器将永远运行。

1
有许多理由支持使用单个消息泵线程而不是使用BackgroundWorker。如果消息泵创建一个线程,它将精确地从其队列中运行一个任务(当然,除非队列为空)。BackgroundWorker可能会尝试启动多个任务;虽然可以使用同步原语来阻止后续任务直到前面的任务完成,但这将使线程池的其他用户饱受饥饿之苦。 - supercat

0

一个有用的模式是让消息泵的外部用户持有一个“仍在使用”标志对象的强引用,而泵本身只持有一个弱引用(一旦该对象的“仍在使用”变得可终结,它的弱引用将无效)。该对象的终结器可能能够向消息泵发送一条消息,而消息泵可以检查其弱引用的持续有效性;如果它已经失效,那么消息泵就可以关闭。

请注意,消息泵的常见困难之一是操作它的线程往往会保持许多仅由该线程使用的对象的存活状态。需要一个单独的对象,线程将避免保持强引用,以确保可以清理事物。


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