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

本文关键字:运行一次 函数 方法 最佳 一次 创建 运行 | 更新日期: 2023-09-27 17:55:34

>我正在尝试创建一个函数,该函数接受操作和超时,并在超时后执行操作。 该功能是非阻塞的。 该函数必须是线程安全的。 我也真的非常非常想避免使用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.Ththread 中我缺少的其他库,但现在我能想到的唯一解决方案是修改上述内容,并每隔一段时间运行专门的清理任务。 有什么建议吗?

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

我不知道

你使用的是哪个版本的C#。但我认为你可以通过使用任务库来完成这一点。然后它看起来像那样。

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

.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);

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

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

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

我的例子:

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();
}

这似乎对我有用。它允许我调用_connection。延迟 15 秒后的 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));

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

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

您使用一次性计时器的模型绝对是要走的路。您当然不想为每个线程创建一个新线程。您可以按时键入单个线程和操作的优先级队列,但这是不必要的复杂性。

在回调中调用Dispose可能不是一个好主意,尽管我很想尝试一下。我似乎记得过去这样做过,效果很好。但我会承认,这是一件有点不稳定的事情。

您只需从集合中删除计时器,而不释放它。如果没有对该对象的引用,它将有资格进行垃圾回收,这意味着 Dispose 方法将由终结器调用。只是没有你想要的那么及时。但这应该不是问题。你只是在短时间内泄漏了一个手柄。只要你没有成千上万的这些东西长时间闲置,这就不会成为问题。

另一种选择是让一个计时器队列保持已分配但已停用(即它们的超时和间隔设置为 Timeout.Infinite )。当您需要计时器时,您可以从队列中拉出一个计时器,对其进行设置,然后将其添加到您的集合中。超时到期后,清除计时器并将其放回队列中。如有必要,您可以动态增加队列,甚至可以不时对其进行整理。

这将防止您为每个事件泄漏一个计时器。相反,您将拥有一个计时器池(很像线程池,不是吗?

使用Microsoft的反应式框架(NuGet "System.Reactive"),然后你可以这样做:

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

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));
}

文档清楚地指出 System.Timers.Timer 具有AutoReset属性,仅用于您的要求:

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

为什么不在异步操作中简单地调用操作参数本身?

Action timeoutMethod = () =>
  {
       Thread.Sleep(timeout_ms);
       action();
  };
timeoutMethod.BeginInvoke();
这可能有点

晚了,但这是我目前用来处理延迟执行的解决方案:

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))