7

In the following code, a Timer is declared inside a function, where it also subscribes to the Elapsed event:

    void StartTimer()
    {
        System.Timers.Timer timer = new System.Timers.Timer(1000);
        timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);
        timer.AutoReset = false;
        timer.Start();
    }

Once the function completes, the reference to the Timer is lost (I presume).

Does the Elapsed event get unregistered automatically as the object is destroyed, or if not, can the event be unregistered in the Elapsed event itself:

    void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        var timer = (System.Timers.Timer)sender;
        timer.Elapsed -= timer_Elapsed;
    }

Would this approach still allow proper cleanup of the object, or should timers never be declared in the local scope of a function for this reason?

4

2 回答 2

15

这里的规则很复杂,你看不到 CLR 内部发生了什么。它维护一个活动计时器列表,一个 System.Timers.Timer 在该列表中有一个引用,可以保持它的活动状态并防止它被垃圾收集。在您的情况下是必要的,因为您的 StartTimer() 方法中的局部变量不足以使其保持活力。

当 AutoReset = false 时,CLR 会在计时时从列表中删除计时器。剩下的唯一引用是 Elapsed 事件处理程序中的sender参数。

如果您没有使用sender显式重新启用计时器,从而将其放回 CLR 队列中,则没有对 Timer 对象的引用。每当 GC 运行时,它将被垃圾收集。

取消订阅 Elapsed 事件处理程序对此没有影响。这是另一个很难看到的细节,您的事件订阅添加了对this的引用。换句话说, Timer 对象实际上使您的外部对象保持活动状态。这当然是一件好事,你不希望你的对象被垃圾收集,而计时器仍然可以调用你的 Elapsed 事件处理程序。如果您希望对象的生命周期不被计时器延长,那么您将不得不做更多的工作。现在有必要显式取消订阅事件处理程序停止计时器。这确实需要您保留对 Timer 对象的引用。

还要记住,如果你的类自己实现了 IDisposable,那么它也应该释放 Timer。这是必要的,因为您通常不希望 Elapsed 事件处理程序在已处置的对象上运行,这往往会触发 ObjectDisposedExceptions。再次将 Timer 对象引用保存在类的字段中的原因。请注意隐藏在地垫下的非常讨厌的线程竞赛错误,Elapsed 事件仍然可以在您调用计时器的 Dispose() 方法之后期间运行。需要联锁以防止您的程序每年或每月因蓝月亮而崩溃一次。当您允许代码在工作线程上运行并访问共享状态时,您必须采取的正常预防措施与其他方面没有什么不同。

总而言之,如果您不再使用 Timer ,那么将其放置在 Elapsed 事件处理程序中是合乎逻辑的事情。这实际上不是必需的,不活动的计时器不会消耗系统资源,但.NET 程序员通常对跳过它非常不舒服。再次,线程竞争是可能的,您可能会处置一个已经处置的计时器,但这不会造成麻烦。

于 2014-07-18T12:07:38.710 回答
2

只需处理计时器:

void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    var timer = (System.Timers.Timer)sender;
    timer.Dispose(); //here
}

没有必要取消事件的挂钩,因为按照惯例,处置始终是完整的处置。

顺便说一句,你在那里写的所有内容都相当于:

await Task.Delay(1000);
于 2014-07-18T10:46:53.493 回答