Object Disposed异常和多线程应用程序

时间:2010-09-04 06:21:59

标签: c# multithreading timer

我有一个启动System.Threading.Timer的应用程序,然后这个计时器每隔5秒从链接数据库读取一些信息并在主要应用程序形式上更新GUI;

由于System.Threading.Timer为Tick事件创建了另一个线程,我需要使用Object.Invoke来更新主应用程序表单上的用户界面,代码如下:

this.Invoke((MethodInvoker)delegate()
  {
       label1.Text = "Example";
  });

应用程序运行良好,但有时当用户关闭主窗体然后关闭应用程序时,如果timer_tick事件上的第二个线程正在更新主线程上的用户界面,则用户将获得ObjectDisposedException。

如何在关闭主窗体之前停止并关闭线程计时器,然后避免对象处置异常?

3 个答案:

答案 0 :(得分:7)

这是一个棘手的主张,因为你必须在给定的Close事件上确保以下内容

  1. 计时器停止。这是相当直接的
  2. 运行代理时不会处理正在更新的控件。再次直截了当。
  3. 当前正在运行计时器刻度的代码已完成。
  4. 这是更难但可行的
  5. 没有待处理的Invoke方法。这完成起来要难得多了
  6. 之前我遇到过这个问题而且我发现防止这个问题很成问题并且涉及很多混乱,难以维护的代码。相反,更容易捕获这种情况可能产生的异常。通常我通过将Invoke方法包装如下来实现:

    static void Invoke(ISynchronizedInvoke invoke, MethodInvoker del) {
      try {
        invoke.Invoke(del,null);
      } catch ( ObjectDisposedException ) {
        // Ignore.  Control is disposed cannot update the UI.
      }
    }
    

    如果你对后果感到满意,忽略这个例外没有任何内在错误。也就是说,如果您已经处理好UI,那么它就不会更新了。我当然是:)

    上述内容并未解决问题#2,但仍需要在您的代理中手动完成。使用WinForms时,我经常使用以下重载来删除该手动检查。

    static void InvokeControlUpdate(Control control, MethodInvoker del) {
      MethodInvoker wrapper = () => {
        if ( !control.IsDisposed ) {
          del();
        }
      };
      try {
        control.Invoke(wrapper,null);
      } catch ( ObjectDisposedException ) {
        // Ignore.  Control is disposed cannot update the UI.
      }
    }
    

    注意

    正如Hans所说,ObjectDisposedException不是唯一可以从Invoke方法中引发的异常。还有其他几个,包括至少InvalidOperationException您需要考虑处理。

答案 1 :(得分:3)

System.Timers.Timer是一个可怕的课程。没有好办法可靠地阻止它,总会有一场比赛,你无法避免它。问题是它的Elapsed事件是从线程池线程中引发的。您无法预测该线程何时实际开始运行。当你调用Stop()方法时,该线程可能已经被添加到线程池中但是还没有开始运行。它受Windows线程调度程序和线程池调度程序的约束。

你甚至无法通过任意延迟关闭窗口来可靠地解决它。在最极端的情况下,线程池调度程序可以将线程的运行延迟最多125秒。你可以通过将关闭延迟几秒来减少异常的可能性,它不会为零。延迟关闭2分钟是不现实的。

只是不要使用它。使用System.Threading.Timer并使其成为您在事件处理程序中重新启动的一次性计时器。或者使用System.Windows.Forms.Timer,它是同步的。

这里应该选择WF Timer,因为您使用的是Control.Invoke()。在UI线程空闲之前,委托目标不会开始运行。与WF计时器完全相同的行为。

答案 2 :(得分:1)

创建两个名为“StopTimer”和“TimerStopped”的布尔值。将计时器的AutoReset属性设置为false。然后将Elapsed方法格式化为以下内容:

TimerStopped = false;
Invoke((MethodInvoker)delegate {
    // Work to do here.
});
if (!StopTimer)
    timer.Start();
else
    TimerStopped = true;

这样就可以防止竞争条件,检查计时器是否应该继续,并在方法结束时进行报告。

现在格式化FormClosing事件,如下所示:

if (!TimerStopped)
{
    StopTimer = true;
    Thread waiter = new Thread(new ThreadStart(delegate {
        while (!TimerStopped) { }
        Invoke((MethodInvoker)delegate { Close(); });
    }));
    waiter.Start();
    e.Cancel = true;
}
else
    timer.Dispose();

如果计时器尚未停止,则启动一个线程等待它完成,然后再次尝试关闭该表单。