.NET 3.5 C#中System.Timer存在Bug导致System.ObjectDisposedException异常:无法访问已释放的对象。

7
在我的Windows服务应用程序中,我经常使用定时器。我只使用System.Timers。我以前从未遇到过这个问题,但突然间我得到了这个异常:
System.ObjectDisposedException: Cannot access a disposed object.
   at System.Threading.TimerBase.ChangeTimer(UInt32 dueTime, UInt32 period)
   at System.Threading.Timer.Change(Int32 dueTime, Int32 period)
   at System.Timers.Timer.UpdateTimer()
   at System.Timers.Timer.set_Interval(Double value)
   at MyApp.MySpace.MySpace2.MyClassWithTimer.MethodChangeTimerInterval()

在我的方法中,我停止了计时器并改变了计时器的间隔。那就是我遇到异常的地方。
我读到了一些关于这个bug的内容,但即使在.NET 3.5中仍然可能存在这个bug吗?
我该如何修复它?我应该在停止计时器后更新计时器对象并将间隔设置为新对象吗?我正在使用GC.KeepAlive(dataTimer);
编辑: 我发现了一些关于这个问题的其他问题:
*我找到了一个链接http://www.kbalertz.com/kb_842793.aspx 基本上,当你停止计时器时,内部的System.Threading.Timer就会变得可用于垃圾回收,有时会导致经过的事件不发生,或者有时会导致已释放引用异常。 虽然文章中没有描述,但我的解决方案是每次停止计时器并重新添加经过的事件时创建一个新的计时器。虽然不高效,但对我来说很容易,而且在处理器方面也不是问题。 这完全解决了我的问题。 感谢所有回应的人。
但我对为什么这个bug仍然存在感到困惑,我需要确定重新添加计时器是一个好主意...
引起错误的代码:
private void StartAsyncResponseTimer()
{
    switch (_lastRequestType)
    {
        case 1:
            asyncResponseTimer.Interval = 1000;
            break;
        case 2:
            asyncResponseTimer.Interval = 2000;
            break;
        case 3:
            asyncResponseTimer.Interval = 3000;
            break;
        default:
            asyncResponseTimer.Interval = 10000;
            break;
    }

    asyncResponseTimer.Start();
}

从SerialPortDataReceived事件调用了该函数:

private void SerialPortDataReceived(object sender, EventArgs e)
{
       StartAsyncResponseTimer();
}

计时器在更改间隔之前已停止。 计时器是我的类的私有字段:
  private Timer asyncResponseTimer = new Timer();

编辑:该应用程序已连续运行数月,这是我第一次遇到此异常!

我的处理模式:

 public class SerialPortCommunication{

 ...

    private void SerialPortDataReceived(object sender, EventArgs e)
    {
        ReadResponse();

        StartAsyncResponseTimer();
    }

    //used to determine if is recieving response over
    private void StartAsyncResponseTimer()
    {
        switch (_lastRequestType)
        {
            case 1:
                asyncResponseTimer.Interval = 1000;
                break;
            case 2:
                asyncResponseTimer.Interval = 2000;
                break;
            case 3:
                asyncResponseTimer.Interval = 3000;
                break;
            default:
                asyncResponseTimer.Interval = 10000;
                break;
        }

        asyncResponseTimer.Start();
    }

    public virtual void Dispose()
    {

        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (!this._disposed)
        {
            if (disposing)
            {
                // Dispose managed resources.

            }

            // Dispose unmanaged resources.
            _disposed = true;

            Stop();

        }
    }

    ~SomeClass()
    {

        Dispose(false);
    }

    #endregion




    public void Stop()
    {
        _asyncResponseTimer.Stop();
        serialPortManager.ClosePort();
    }
}

2
说实话,我在使用计时器时的技巧是要在某个地方保留对它们的引用(例如私有字段或计时器集合),这样我就知道仍然存在引用。这样我就知道在我想要的时候它们不会被垃圾回收。当然,当你完成计时器对象后,你必须确保删除该引用。 - Chris
1
引用的错误早已修复。请检查您的代码中的Dispose()调用和using语句。对已知工作版本的代码进行差异比较应该会有所帮助。 - Hans Passant
我不能发布完整的类,因为其中有很多专业知识。但是在类中,我不使用using语句。我可以发布我的dispose模式,请查看更新。 - Simon
请问你能帮我吗?我该怎么办?捕获异常并重新启动计时器?使用更多的GC.KeepAlive?我没有使用“using”语句,也没有在我的dispose实现中手动释放计时器方法。我应该使用一些计时器集合吗?我只有私有成员初始化。我也无法区分已知工作版本,因为实际工作版本存在此问题,并且在过去两个月中我没有更改任何内容,当我的Windows服务正在运行时。你能给我一些建议吗?拜托了。 - Simon
1
创建一个新项目并构建一个基本项目,可以重现该错误并显示相应的代码。 - s_hewitt
显示剩余2条评论
3个回答

1

由于您没有指定服务器配置,我假设使用的是Windows Server 2003。当您提到它已经工作了(几乎)两个月时,这让我想起了49.7天bug。除非在三月份再次崩溃,否则它可能不适用于您的情况。 :-)

症状2
在一个未运行ISA Server的基于Windows Server 2003的计算机上,在以下条件为真时会出现类似的问题:在应用程序中重复调用CreateTimerQueueTimer函数;设置一个特定的时间间隔来触发由CreateTimerQueueTimer函数创建的计时器。

该应用程序运行时间超过49.7天。在49.7天后,计时器立即被触发,而不是在指定的时间间隔之后触发。几分钟后,计时器会正确地被触发。

在此处KeepAlive()无用,因为您将计时器声明为类级别,这是很好的。更改时间间隔不需要停止计时器。

从调用栈和提供的代码来看,似乎您正在尝试更改已处置计时器的间隔。您可能需要在确认没有更多工作要做后停止计时器,而不是在处理中止模式中停止它,因为这似乎会干扰正在进行的工作。也就是说,首先检查是否还有工作要做,如果没有,则停止监听端口,然后停止计时器。除非我可以看到更多代码的其他部分 - 如事件处理程序、serialPortManager声明等,否则这并没有什么帮助。

1

你是不是在销毁计时器后立即获取串口数据?这是我能想到的唯一可能,根据你发布的数据!在Dispose()方法中的Stop()!方法中你在做什么?


我从未调用过函数timer.Dispose()。我需要Stop()来停止串口通信。我还有一个方法Start()。当我处置SerialPortCommunication类时,我也会停止计时器以防止在处理后发生异常,因为它在不同的线程上。那个异常并没有在处理时引发。它发生在与串口通信的中途。每次发生SerialPortDataReceived()事件时都会调用重置计时器间隔。 - Simon

1

看起来你在串口接收到数据后启动了计时器。如果在计时器完成发送响应之前,又接收到了额外的数据,会发生什么?根据你发布的信息,似乎计时器间隔将被更改并重新启动(再次)而计时器仍在处理其计时事件。

你考虑过上述情况吗?计时器是自动重置的吗?你何时调用Stop()方法?


计时器的作用是确定是否没有接收到数据。因此,每次发生serialPortDataReceived事件时都会重新启动计时器。如果计时器完成了它的工作,我就知道所有的数据都已经接收完毕,可以继续发送请求。计时器默认的AutoReset=true。只有当我想停止串口通信时才会调用Stop(),并且当我得到那个异常时,Stop()没有被调用。基本上是由用户从UI中调用的。 - Simon

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