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

时间:2011-05-05 22:01:51

标签: c# timer

我正在尝试创建一个接受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中的其他一些库我想念,但是现在我能想到的唯一解决方案通过在一个间隔上运行的专用清理任务来修改上述内容。有什么建议吗?

12 个答案:

答案 0 :(得分:65)

我不知道您使用的是哪个版本的C#。但我认为你可以通过使用任务库来实现这一目标。它会看起来像那样。

public class PauseAndExecuter
{
    public async Task Execute(Action action, int timeoutInMilliseconds)
    {
        await Task.Delay(timeoutInMilliseconds);
        action();
    }
}

答案 1 :(得分:26)

.Net 4没有任何内置功能可以很好地完成这项工作。 Thread.Sleep甚至AutoResetEvent.WaitOne(超时)都不好 - 它们会占用线程池资源,我已经烧了试试这个!

最轻的解决方案是使用计时器 - 特别是如果你有很多任务要投入它。

首先制作一个简单的计划任务类:

class ScheduledTask
{
    internal readonly Action Action;
    internal System.Timers.Timer Timer;
    internal EventHandler TaskComplete;

    public ScheduledTask(Action action, int timeoutMs)
    {
        Action = action;
        Timer = new System.Timers.Timer() { Interval = timeoutMs };
        Timer.Elapsed += TimerElapsed;            
    }

    private void TimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        Timer.Stop();
        Timer.Elapsed -= TimerElapsed;
        Timer = null;

        Action();
        TaskComplete(this, EventArgs.Empty);
    }
}

然后,创建一个调度程序类 - 再次,非常简单:

class Scheduler
{        
    private readonly ConcurrentDictionary<Action, ScheduledTask> _scheduledTasks = new ConcurrentDictionary<Action, ScheduledTask>();

    public void Execute(Action action, int timeoutMs)
    {
        var task = new ScheduledTask(action, timeoutMs);
        task.TaskComplete += RemoveTask;
        _scheduledTasks.TryAdd(action, task);
        task.Timer.Start();
    }

    private void RemoveTask(object sender, EventArgs e)
    {
        var task = (ScheduledTask) sender;
        task.TaskComplete -= RemoveTask;
        ScheduledTask deleted;
        _scheduledTasks.TryRemove(task.Action, out deleted);
    }
}

可以按如下方式调用 - 并且非常轻量级:

var scheduler = new Scheduler();

scheduler.Execute(() => MessageBox.Show("hi1"), 1000);
scheduler.Execute(() => MessageBox.Show("hi2"), 2000);
scheduler.Execute(() => MessageBox.Show("hi3"), 3000);
scheduler.Execute(() => MessageBox.Show("hi4"), 4000);

答案 2 :(得分:5)

我的例子:

void startTimerOnce()
{
   Timer tmrOnce = new Timer();
   tmrOnce.Tick += tmrOnce_Tick;
   tmrOnce.Interval = 2000;
   tmrOnce.Start();
}

void tmrOnce_Tick(object sender, EventArgs e)
{
   //...
   ((Timer)sender).Dispose();
}

答案 3 :(得分:4)

我使用此方法在特定时间安排任务:

public void ScheduleExecute(Action action, DateTime ExecutionTime)
{
    Task WaitTask = Task.Delay(ExecutionTime.Subtract(DateTime.Now));
    WaitTask.ContinueWith(() => action());
    WaitTask.Start();
}

应该注意的是,由于int32最大值,这仅适用于大约24天。

答案 4 :(得分:2)

如果您不太关心时间的粒度,可以创建一个每秒滴答的计时器,并检查需要在ThreadPool上排队的过期操作。只需使用秒表类检查超时。

您可以使用当前的方法,但您的词典将使用秒表作为其键,将操作作为其值。然后,您只需遍历所有KeyValuePairs并找到过期的秒表,对Action进行排队,然后将其删除。然而,您将从LinkedList获得更好的性能和内存使用(因为您每次都会枚举整个内容并且删除项目更容易)。

答案 5 :(得分:2)

你拥有的模型,使用一次性计时器,绝对是你要走的路。你当然不想为它们中的每一个创建一个新线程。你可以有一个单独的线程和一个按时关键的动作优先级队列,但那是不必要的复杂性。

在回调中调用Dispose可能不是一个好主意,尽管我很想尝试一下。我似乎记得在过去这样做,它运作正常。但我承认,这是一件不可思议的事情。

您可以从集合中删除计时器,而不是将其丢弃。如果没有对该对象的引用,它将有资格进行垃圾回收,这意味着终结器将调用Dispose方法 。只是不像你想的那么及时。但这应该不是问题。你只是在短时间内泄漏手柄。只要你没有成千上万的这些东西长时间处于未配置状态,就不会有问题。

另一种选择是让一个定时器队列保持分配,但是停用(即它们的超时和间隔设置为Timeout.Infinite)。当您需要计时器时,从队列中提取一个计时器,进行设置并将其添加到您的集合中。超时到期后,清除计时器并将其重新放回队列。如果必须,您可以动态增长队列,甚至可以不时地修改它。

这将阻止您为每个事件泄漏一个计时器。相反,你会有一个定时器池(很像线程池,没有?)。

答案 6 :(得分:1)

treze的代码工作得很好。这可能有助于那些必须使用旧.NET版本的人:

private static volatile List<System.Threading.Timer> _timers = new List<System.Threading.Timer>();
private static object lockobj = new object();
public static void SetTimeout(Action action, int delayInMilliseconds)
{
    System.Threading.Timer timer = null;
    var cb = new System.Threading.TimerCallback((state) =>
    {
        lock (lockobj)
            _timers.Remove(timer);
        timer.Dispose();
        action();
    });
    lock (lockobj)
        _timers.Add(timer = new System.Threading.Timer(cb, null, delayInMilliseconds, System.Threading.Timeout.Infinite));
}

答案 7 :(得分:1)

文档明确指出System.Timers.Timer具有AutoReset属性,仅针对您的要求:

https://msdn.microsoft.com/en-us/library/system.timers.timer.autoreset(v=vs.110).aspx

答案 8 :(得分:1)

这似乎对我有用。它允许我在15秒延迟后调用_connection.Start()。 -1毫秒参数只是说不重复。

// Instance or static holder that won't get garbage collected (thanks chuu)
System.Threading.Timer t;

// Then when you need to delay something
var t = new System.Threading.Timer(o =>
            {
                _connection.Start(); 
            },
            null,
            TimeSpan.FromSeconds(15),
            TimeSpan.FromMilliseconds(-1));

答案 9 :(得分:1)

使用Microsoft的Reactive Framework(NuGet“ System.Reactive”),然后可以执行以下操作:

protected void Execute(Action action, int timeout_ms)
{
    Scheduler.Default.Schedule(TimeSpan.FromMilliseconds(timeout_ms), action);
}

答案 10 :(得分:0)

为什么不在异步操作中简单地调用您的action参数呢?

Action timeoutMethod = () =>
  {
       Thread.Sleep(timeout_ms);
       action();
  };

timeoutMethod.BeginInvoke();

答案 11 :(得分:-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))