【问题标题】:How do I gracefully stop a System.Threading.Timer?如何优雅地停止 System.Threading.Timer?
【发布时间】:2022-01-25 12:18:03
【问题描述】:

我有一个用 C# 实现的 Windows 服务,它需要经常做一些工作。我已经使用System.Threading.Timer 和一个负责调度下一个回调的回调方法实现了这一点。我无法优雅地停止(即处置)计时器。这是一些可以在控制台应用程序中运行的简化代码,可以说明我的问题:

const int tickInterval = 1000; // one second

timer = new Timer( state => {
                       // simulate some work that takes ten seconds
                       Thread.Sleep( tickInterval * 10 );

                       // when the work is done, schedule the next callback in one second
                       timer.Change( tickInterval, Timeout.Infinite );
                   },
                   null,
                   tickInterval, // first callback in one second
                   Timeout.Infinite );

// simulate the Windows Service happily running for a while before the user tells it to stop
Thread.Sleep( tickInterval * 3 );

// try to gracefully dispose the timer while a callback is in progress
var waitHandle = new ManualResetEvent( false );
timer.Dispose( waitHandle );
waitHandle.WaitOne();

问题是当waitHandle.WaitOne 阻塞时,我在回调线程上从timer.Change 得到一个ObjectDisposedException。我做错了什么?

The documentation 表示我正在使用的 Dispose 重载:

在所有当前排队的回调完成之前,不会释放计时器。

编辑:看来文档中的此声明可能不正确。有人可以验证吗?

我知道我可以通过在回调和处置代码之间添加一些信号来解决这个问题,正如 Henk Holterman 下面建议的那样,但除非绝对必要,否则我不想这样做。

【问题讨论】:

  • 为什么不能让定时器每 10 秒自动运行一次呢?为什么要手动重新安排?
  • @Tudor:因为有时工作需要更长的时间,而且我不希望多个回调在它们的执行中重叠。
  • 服务停止时为什么要手动暴露定时器?当 AppDomain 卸载时,所有内存都会被回收。
  • @DanielHilgarth:在我的实际实现中,我正在处理来自ServiceBase.OnStop 的计时器。在我让服务关闭之前,我想确保计时器已被释放并且没有正在进行回调。我还有其他需要运行的关闭代码,在确定计时器完全消失之前我不想这样做。
  • 我开始怀疑你是否遇到了一个与线程回调引用相关的微妙问题,作为构造函数的匿名委托......如果你(为了测试)将它转换为命名函数...鉴于计时器在辅助 CLR 线程上触发..

标签: c# .net multithreading


【解决方案1】:

使用此代码

 timer = new Timer( state => {
                   // simulate some work that takes ten seconds
                   Thread.Sleep( tickInterval * 10 );

                   // when the work is done, schedule the next callback in one second
                   timer.Change( tickInterval, Timeout.Infinite );
               },
               null,
               tickInterval, // first callback in one second
               Timeout.Infinite );

几乎可以肯定,您会在计时器处于休眠状态时对其进行处置。

您必须在 Sleep() 之后保护代码以检测 Disposed 计时器。由于没有 IsDisposed 属性,因此快速而肮脏的static bool stopping = false; 可能会成功。

【讨论】:

  • 谢谢;我只是认为我不需要做这样的事情,因为文档指出“在所有当前排队的回调完成之前,计时器不会被释放。”这句话是什么意思?
  • 这句话意味着你应该是对的,但我在 timer.Change() 上看到了 ObjectDisposed 异常。无论如何都要摆脱那个,还有其他方法可以防止重新进入。
  • 想知道“排队回调”这个短语的字面意思是否可能是挂起但当前未执行的附加回调,例如,根据定义,当前正在执行的回调未排队?
  • @DavidW:是的,也许吧。我现在只是通过在回调中从timer.Change 捕获和抑制ObjectDisposedException 来解决这个问题。也许最终微软的人会看到这一点并给我一个更好的答案。
  • 您似乎有一个与文档相矛盾的可重现案例,请考虑将其发布到 MS Connect
【解决方案2】:

如“Windows 上的并发编程”中所述:
创建一个虚拟类 InvalidWaitHandle,继承自 WaitHandle:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Threading;

namespace MyNameSpace
{
    class InvalidWaitHandle : WaitHandle
    {

    }
}

因此您可以像这样正确处理 System.Threading.Timer:

public static void DisposeTimer()
{
   MyTimer.Dispose(new InvalidWaitHandle());
   MyTimer = null;
}

【讨论】:

    【解决方案3】:

    保护回调方法不使用已处理的计时器的可能解决方案:

    ManualResetEvent waitHandle = new ManualResetEvent(false);
    if (!timer.Dispose(waitHandle) || waitHandle.WaitOne((int)timeout.TotalMilliseconds)
    {
        waitHandle.Close();  // Only close when not timeout
    }
    

    另请参阅:https://stackoverflow.com/a/15902261/193178

    【讨论】:

      【解决方案4】:

      您无需处理计时器即可停止它。您可以调用Timer.Stop() 或将Timer.Enabled 设置为false,这两种方法都会停止计时器的运行。

      【讨论】:

      • 我相信这些是System.Timers.Timer 的成员,而不是我正在使用的System.Threading.Timer
      猜你喜欢
      • 2021-06-04
      • 2010-11-05
      • 1970-01-01
      • 1970-01-01
      • 2011-03-12
      • 2018-09-27
      • 2019-11-10
      • 1970-01-01
      相关资源
      最近更新 更多