为什么Windows.System.Threading.ThreadPoolTimer.Cancel()不起作用

本文关键字:不起作用 Cancel ThreadPoolTimer Windows System Threading 为什么 | 更新日期: 2023-09-27 18:11:26

更新:此操作在Windows 10中正常工作

下面是一个简单的例子:

    void testcase()
    {
         if (myTimer != null)
             myTimer.Cancel();
         myTimer = ThreadPoolTimer.CreateTimer(
             t => myMethod(),
             TimeSpan.FromMilliseconds(4000)
         );
    }
    void myMethod()
    {
         myTimer = null;
         //some work
    }

它应该做的是确保myMethod不能被更频繁地调用,而且如果已经有对testcase的新调用,则不应该调用myMethod。类似于桌面的。net计时器是可能的。但是,对testcase的新调用不会阻止先前计划的myMethods运行。我有一个简单的解决方法,将整数callid参数添加到myMethod并跟踪它。但是上面这个应该可以工作,但它没有。

我做错了什么吗?还有人有更好的主意吗?

为什么Windows.System.Threading.ThreadPoolTimer.Cancel()不起作用

你正在寻找的东西被称为debouting,至少在javascript中是这样。

实现它的一个简单方法是使用System.Threading.Timer代替,它有一个方便的Change用来重置它。

如果你想把它抽象到你自己的定时器类中,它看起来像这样:

public class DebounceTimer : IDisposable
{
    private readonly System.Threading.Timer _timer;
    private readonly int _delayInMs;
    public DebounceTimer(Action callback, int delayInMs)
    {
        _delayInMs = delayInMs;
        // the timer is initially stopped
        _timer = new System.Threading.Timer(
            callback: _ => callback(),
            state: null,
            dueTime: System.Threading.Timeout.Infinite, 
            period: System.Threading.Timeout.Infinite);
    }
    public void Reset()
    {
        // each call to Reset() resets the timer
        _timer.Change(
            dueTime: _delayInMs,
            period: System.Threading.Timeout.Infinite);
    }
    public void Dispose()
    {
        // timers should be disposed when you're done using them
        _timer.Dispose();
    }
}

您的测试用例将变成:

private DebounceTimer _timer;
void Init()
{
    // myMethod will be called 4000ms after the
    // last call to _timer.Reset()
    _timer = new DebounceTimer(myMethod, 4000);
}
void testcase()
{
    _timer.Reset();
}
void myMethod()
{
    //some work
}
public void Dispose()
{
    // don't forget to cleanup when you're finished testing
    _timer.Dispose();
}
(更新)

从你的评论来看,似乎你想在每次重置时更改回调方法,并且只调用最后一个。如果是这种情况,您可以将代码更改为:

class DebounceTimer : IDisposable
{
    private readonly System.Threading.Timer _timer;
    private readonly int _delayInMs;
    private Action _lastCallback = () => { };
    public DebounceTimer(int delayInMs)
    {
        _delayInMs = delayInMs;
        // the timer is initially stopped
        _timer = new System.Threading.Timer(
            callback: _ => _lastCallback(),
            state: null,
            dueTime: System.Threading.Timeout.Infinite, 
            period: System.Threading.Timeout.Infinite);
    }
    public void Reset(Action callback)
    {
        _timer.Change(dueTime: _delayInMs, period: System.Threading.Timeout.Infinite);
        // note: no thread synchronization is taken into account here,
        // a race condition might occur where the same callback would
        // be executed twice
        _lastCallback = callback;
    }
    public void Dispose()
    {
        _timer.Dispose();
    }
}

当调用Reset方法时,您可以使用lambda来捕获各种方法调用(不仅仅是Action方法):

void testcase()
{
    _timer.Reset(() => myMethod());
}
void othertestcase()
{
    // it's still a parameterless action, but it
    // calls another method with two parameters
    _timer.Reset(() => someOtherMethod(x, y));
}

正如第二个计时器片段的注释中所述,该代码不是线程安全的,因为当回调引用在Reset方法中被更改时,计时器处理程序可能已经在一个单独的线程上运行(或即将运行),这意味着相同的回调将被执行两次。

一个稍微复杂一点的解决方案是在更改回调时进行锁定,并进行额外的检查,如果从上次调用到重置已经经过了足够的时间。最后的代码看起来像这样(可能还有其他同步的方法,但这个方法非常简单):

class DebounceTimer : IDisposable
{
    private readonly System.Threading.Timer _timer;
    private readonly int _delayInMs;
    private readonly object _lock = new object();
    private DateTime _lastResetTime = DateTime.MinValue;
    private Action _lastCallback = () => { };
    public DebounceTimer(int delayInMs)
    {
        _delayInMs = delayInMs;
        // the timer is initially stopped
        _timer = new System.Threading.Timer(
            callback: _ => InvokeIfTimeElapsed(),
            state: null,
            dueTime: System.Threading.Timeout.Infinite, 
            period: System.Threading.Timeout.Infinite);
    }
    private void InvokeIfTimeElapsed()
    {
        Action callback;
        lock (_lock)
        {
            // if reset just happened, skip the whole thing
            if ((DateTime.UtcNow - _lastResetTime).TotalMilliseconds < _delayInMs)
                return;
            else
                callback = _lastCallback;
        }
        // if we're here, we are sure we've got the right callback - invoke it.
        // (even if reset happens now, we captured the previous callback 
        // inside the lock)
        callback();
    }
    public void Reset(Action callback)
    {
        lock (_lock)
        {
            // reset timer
            _timer.Change(
                dueTime: _delayInMs,
                period: System.Threading.Timeout.Infinite);
            // save last reset timestamp
            _lastResetTime = DateTime.UtcNow;
            // set the new callback
            _lastCallback = callback;
        }
    }
    public void Dispose()
    {
        _timer.Dispose();
    }
}

问题是你在myMethod中设置了timer = null。这保证了它将在下一次调用testCase时为空(所以它不会被取消)。

应该使用TimerPool。CreateTimer创建单实例计时器。它只会发射一次。当工作进程结束时,它应该做的最后一件事是初始化一个新的计时器。

为了回答我自己可能的问题,似乎Cancel()仅用于取消周期计时器的进一步重复。我不能说文档中是这么说的,但它似乎是这样工作的。因此,如果timer不是周期性的,则Cancel不起作用。