在 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#。但我认为你可以通过使用任务库来完成这一点。然后它看起来像那样。
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))