65.9K
CodeProject 正在变化。 阅读更多。
Home

如何关闭多线程 .NET Windows Forms 应用程序并防止抛出 ObjectDisposedException

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.50/5 (3投票s)

2008年11月15日

LGPL3

4分钟阅读

viewsIcon

69793

downloadIcon

958

本文介绍如何正确关闭多线程 Windows Forms 应用程序,同时避免抛出 ObjectDisposedException。

假设

假设读者了解 .NET Windows Forms 事件处理和多线程知识,包括使用 lock 语句以及如何将事件委托给 GUI 线程。

概述

本文解释了如何正确关闭一个多线程的 .NET Windows Forms 应用程序,该应用程序有一个在后台运行的线程,该线程会触发事件来更新或修改 GUI。如果此类应用程序没有通过代码正确关闭,可能出现的主要问题是抛出 ObjectDisposedException,并带有消息“无法访问已释放的对象”。出现此错误是因为 GUI 在后台线程有机会关闭之前就被释放了,导致触发了一个尝试更新已释放对象的事件。导致问题的关键在于 GUI 比后台线程先被释放,以及后台线程触发了更新 GUI 的事件。请注意,如果后台线程在 GUI 之前关闭,则不会出现此问题,这也是您可能注意到此问题并非每次都发生的原因。

下图展示了问题的发生情况

Problem_ThreadDiagram.JPG

问题

以下代码展示了如何重现该问题。

void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    // Signal the BackgroundWorkerThread to end
    this.CloseBackgroundWorker = true;
}
void BackgroundWorkerThread()
{
    // Check to see if the background worker is signaled
    // to end
    while (this.CloseBackgroundWorker != true)
    {
        // Fire an event that updates the GUI
        SomeEvent(null, null);

        // Timeout (kept small on purpose)
        Thread.Sleep(1);
    }
}
void Form1_SomeEvent(object sender, EventArgs e)
{
    if (this.InvokeRequired == true)
    {
        // Invoke the event onto the GUI thread
        EventHandler eh = new EventHandler(Form1_SomeEvent);
        this.Invoke(eh, new object[] { null, null });
    }
    else
    {
        // Event handler code that updates the GUI
        label1.Text = "Counter value: " + _counter++;
    }
}

基本上,当用户指示应用程序关闭时,后台运行的线程 BackgroundWorkerThread 会收到关闭信号,而 GUI Form1 会被释放。然而,问题在于,不能保证 BackgroundWorkerThread 会在 Form1 被释放之前关闭。这是一个大问题,因为如果 BackgroundWorkerThreadForm1 被释放后运行了很短的时间,那么更新 GUI 的事件 SomeEvent 很可能会被触发(请参阅 Form1_SomeEvent 事件处理程序中 label1.Text 的更改)。而此时 Form1 已经被释放,无法进行更新,从而会抛出 ObjectDisposedException 异常。

要重现此行为,只需下载、构建并运行应用程序,然后单击退出按钮关闭应用程序。如前所述,该异常可能不会每次都发生,因此如果没有任何反应,请再次尝试运行应用程序。请注意,BackgroundWorkerThread 中的 while 循环有一个非常短的超时时间,以增加抛出异常的几率。

解决方案

这个问题的解决方案概念上很简单。当用户尝试关闭 GUI 时,GUI 应该首先等待后台运行的线程关闭,然后在该过程完成后,再开始关闭自身。因此,在上面示例代码中,Form1 应该首先等待 BackgroundWorkerThread 关闭,只有在发生这种情况后,才继续关闭自身。

下图展示了解决方案

Solution_ThreadDiagram.JPG

以下是解决方案的代码

bool _safeToCloseGUI = false;
void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    // Signal the BackgroundWorkerThread to end
    this.CloseBackgroundWorker = true;

    // The _safeToCloseGUI flag will  be true when the
    // background worker thread has finished
    if (_safeToCloseGUI == false)
    {
        // The background worker thread has not closed yet,
        // so don't close the GUI yet
        e.Cancel = true;
    }
}

void BackgroundWorkerThread()
{
    // Check to see if the background worker is signaled
    // to end
    while (this.CloseBackgroundWorker != true)
    {
        // Fire an event that updates the GUI
        SomeEvent(null, null);

        // Timeout (kept small on purpose)
        Thread.Sleep(1);
    }

    // If we get here, it means that the background worker
    // thread is finished doing its work. It is now safe
    // to shutdown the GUI (since SomeEvent cannot be called
    // anymore). The GUI must be closed via an event because
    // this code is running in a background thread.

    // Fire the event to close the GUI
    CloseGUI(null, null);
}

void Form1_SomeEvent(object sender, EventArgs e)
{
    if (this.InvokeRequired == true)
    {
        // Invoke the event onto the GUI thread
        EventHandler eh = new EventHandler(Form1_SomeEvent);
        this.Invoke(eh, new object[] { null, null });
    }
    else
    {
        // Event handler code that updates the GUI
        label1.Text = "Counter value: " + _counter++;
    }
}
void Form1_CloseGUI(object sender, EventArgs e)
{
    if (this.InvokeRequired == true)
    {
        // Invoke the event onto the GUI thread
        EventHandler eh = new EventHandler(CloseGUI);
        this.Invoke(eh, new object[] { null, null });
    }
    else
    {
        // Event handler code that tells the GUI it is
        // safe to close, then closes it
        _safeToCloseGUI = true;
        this.Close();
    }
}

对原始代码做了一些修改。

首先,在 Form1_FormClosing 方法中添加了一个新标志 _safeToCloseGUI,只有当该标志设置为 true 时才能关闭 GUI。当用户单击关闭应用程序时,后台工作线程将像以前一样收到关闭信号。但是,现在 GUI 的关闭操作将被取消,因为 _safeToCloseGUI 标志最初设置为 false。接下来将看到,一旦 GUI 可以安全关闭,此标志将被设置为 true

接下来,修改了 BackgroundWorkerThread,使其一旦收到关闭信号,就会跳出 while 循环,然后触发一个事件来告知 GUI 关闭。在 BackgroundWorkerThread 跳出 while 循环后,可以保证它不会触发更新 GUI 的事件,因此此时 GUI 可以安全关闭。因此,BackgroundWorkderThread 将触发 CloseGUI 事件来告知 GUI 关闭。

最后,添加了事件处理程序 Form1_CloseGUI 来处理 CloseGUI 事件。首先,处理程序将 _safeToCloseGUI 标志设置为 true,然后调用 Close 方法。这将导致 Form1_FormClosing 继续执行,因为 _safeToCloseGUI 现在为 true,GUI 将安全且成功地关闭。

替代方案

以下是另一种解决此问题的方法,代码量大大减少,尽管解决方案不够优雅。基本上,可以在更新 GUI 的 invoke 行周围放置一个 try/catch 块,这样如果抛出异常,它将被处理,应用程序也不会崩溃。请参阅以下代码

void Form1_SomeEvent(object sender, EventArgs e)
{
    if (this.InvokeRequired == true)
    {
        // Invoke the event onto the GUI thread
        EventHandler eh = new EventHandler(Form1_SomeEvent);
        try
        {
            this.Invoke(eh, new object[] { null, null });
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }
    else
    {
        // Event handler code that updates the GUI
        label1.Text = "Counter value: " + _counter++;
    }
}

这种方法不够优雅,因为应用程序退出时仍然会发生异常(尽管已被处理且不会崩溃)。但是,如果您想完全避免异常,推荐的方法是最初提出的解决方案。最初的解决方案可能需要更多的工作,但我认为它才是“更整洁”的解决方案。

© . All rights reserved.