如何优雅地停止System.Threading.Timer?

18

我用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();
我在回调线程中使用timer.Change时,会出现ObjectDisposedException,而waitHandle.WaitOne正在阻塞。我做错了什么?
我所使用的Dispose重载的文档指出:
“直到所有当前排队的回调完成之前,定时器才不会被处理。”
编辑:似乎文档中的这种说法可能是不正确的。有人可以验证吗?
我知道我可以通过添加一些信号来解决回调和清理代码之间的问题,就像Henk Holterman在下面建议的那样,但除非绝对必要,否则我不想这样做。

为什么你不能让计时器自己每10秒运行一次?为什么要手动重新安排它? - Tudor
1
@Tudor: 有时候工作需要更长的时间,而我不希望多个回调在执行过程中重叠。 - William Gross
@DanielHilgarth:在我的实际实现中,我是从ServiceBase.OnStop中进行计时器处理的。我想确保计时器被处理并且没有回调正在进行,然后才允许服务关闭。我还有其他关闭代码需要运行,但在确保计时器完全消失之前,我不想这样做。 - William Gross
@DavidW:在我实际的Windows服务实现中,回调确实是一个命名函数,但我仍然遇到了同样的问题。 - William Gross
这是由于已记录的竞态条件引起的。你可能想看一下我在这里的答案,因为它展示了如何处理这个问题。 - BatteryBackupUnit
显示剩余3条评论
4个回答

12

使用这段代码

 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() 后检测到已经被处理的计时器。由于没有 IsDisposed 属性,一个快速而简单的 static bool stopping = false; 可能会解决问题。


谢谢。我只是认为我不需要这样做,因为文档中写道:“计时器直到所有当前排队的回调完成后才会被处理”。这句话是什么意思? - William Gross
那句话的意思是你应该是正确的,但我在timer.Change()上看到了一个ObjectDisposed异常。无论如何,要摆脱它,还有其他方法可以防止重入。 - H H
3
想知道“排队回调”一词是否字面意思为可能未执行但当前未在执行的额外回调,例如,按定义,正在执行的回调不在队列中? - David W
@DavidW:是的,也许吧。我现在只是通过在回调函数中捕获和抑制 timer.Change 中的 ObjectDisposedException 来解决这个问题。也许最终微软的某个人会看到这个并给我一个更好的答案。 - William Gross
1
你似乎有一个可以重现的案例与文档相矛盾,考虑在MS Connect上发布它。 - H H

0

保护回调方法不受已释放计时器影响的可能解决方案:

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

另请参阅:https://dev59.com/0Ww15IYBdhLWcg3w0fAV#15902261


0
如《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 的 Dispose:
public static void DisposeTimer()
{
   MyTimer.Dispose(new InvalidWaitHandle());
   MyTimer = null;
}

虚拟的WaitHandle的目的是什么? - undefined

-4

您不需要处理计时器即可停止它。您可以调用 Timer.Stop() 或将 Timer.Enabled 设置为 false,两者都可以停止计时器运行。


3
我相信那些是 System.Timers.Timer 的成员,而不是我使用的 System.Threading.Timer - William Gross

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