在C#中创建“仅运行一次”延迟函数的最佳方法

53
我正在尝试创建一个函数,该函数接收一个Action和一个Timeout,并在Timeout后执行Action。该函数应该是非阻塞的,并且必须是线程安全的。我也非常想避免使用Thread.Sleep()。
到目前为止,我能做到最好的是这样的:
long currentKey = 0;
ConcurrentDictionary<long, Timer> timers = new ConcurrentDictionary<long, Timer>();

protected void Execute(Action action, int timeout_ms)
{
    long currentKey = Interlocked.Increment(ref currentKey);
    Timer t = new Timer(
      (key) =>
         {
           action();
           Timer lTimer;
           if(timers.TryRemove((long)key, out lTimer))
           {
               lTimer.Dispose();
           }
         }, currentKey, Timeout.Infinite, Timeout.Infinite
      );

     timers[currentKey] = t;
     t.Change(timeout_ms, Timeout.Infinite);
}

问题在于从回调函数中调用Dispose()可能不安全。我不确定直接结束是否安全,即使定时器在执行其lambda表达式时仍被认为是活动的,但即使如此,我仍然更愿意妥善处理它。
“延迟一次触发”似乎是一个常见的问题,应该有一种简单的方法来解决这个问题,可能是System.Threading中的某个其他库,但目前我能想到的唯一解决方案是修改上述代码,并在间隔上运行专门的清理任务。您有什么建议吗?

1
我有点看不懂为什么你不用最简单的解决方案来解决这个问题,也就是Thread.Sleep? - jeroenh
你能解释一下为什么你真的、真的想避免使用Thread.Sleep()吗? - Andrew Savinykh
1
使用此代码的任何人请注意!Timer API 中存在一个错误,如果定时器过于细粒度(即在 .Change() 方法之后太快调用 Dispose()),可能会导致未管理的内存泄漏。截至本评论,.NET 4.0 是最新版本,希望在将来的版本中修复此问题。 - Chuu
7
如果只有一两个操作,使用Thread.Sleep可能可行。但是如果你有100个操作呢?你真的想创建100个什么也不做,只睡眠的线程吗?这将浪费100兆字节的堆栈空间(以及其他资源)。 - Jim Mischel
1
@JimMischel 我已经学会了 :-); 感谢您指出。 - jeroenh
12个回答

0
为什么不在异步操作中直接调用您的动作参数本身呢?
Action timeoutMethod = () =>
  {
       Thread.Sleep(timeout_ms);
       action();
  };

timeoutMethod.BeginInvoke();

BeginInvoke() 使用线程池,因此它与其他提出的解决方案存在类似的问题。 - Chuu

-2

可能有点晚了,但这是我目前使用的处理延迟执行的解决方案:

public class OneShotTimer
{

    private volatile readonly Action _callback;
    private OneShotTimer(Action callback, long msTime)
    {
        _callback = callback;
        var timer = new Threading.Timer(TimerProc);
        timer.Change(msTime, Threading.Timeout.Infinite);
    }

    private void TimerProc(object state)
    {
        try {
            // The state object is the Timer object. 
            ((Threading.Timer)state).Dispose();
            _callback.Invoke();
        } catch (Exception ex) {
            // Handle unhandled exceptions
        }
    }

    public static OneShotTimer Start(Action callback, TimeSpan time)
    {
        return new OneShotTimer(callback, Convert.ToInt64(time.TotalMilliseconds));
    }
    public static OneShotTimer Start(Action callback, long msTime)
    {
        return new OneShotTimer(callback, msTime);
    }
}

你可以像这样使用它:

OneShotTimer.Start(() => DoStuff(), TimeSpan.FromSeconds(1))

@user2864740 你说得对。它从VB翻译得很差。 - Karsten
@user2864740 2) 你能详细说明一下吗?非释放的计时器对象实例在回调触发后是必要的,还是你指的是构造函数?我认为计时器不能被垃圾回收,因为它必须从某个地方引用才能最终执行回调。 - Karsten
@user2864740 ...并将计时器作为状态对象传递给回调函数。 - Karsten
那么这个“状态”在哪里设置?它不是在给定的代码中完成的,因此存在严重的语义错误。此外,我并不完全确定通过状态本身传递计时器将确保强根引用:但是,在建议的代码利用状态尝试维护强引用之前,这与问题无关。 - user2864740
@user2864740 这并不完全正确。计时器是使用全局 计时器队列 实现的,它实际上会保持对计时器及其状态对象的强引用。因此,它将正常工作。我刚刚检查了一下,您可以随意进行垃圾回收,回调函数始终会被调用,没有任何问题。您唯一可能会抱怨的是文档中没有保证。 - Karsten
显示剩余3条评论

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