timer-在C#中创建“一次运行”延时功能的最佳方法
我正在尝试创建一个接受Action和Timeout并在Timeout之后执行Action的函数。 该功能是非阻塞的。 该函数必须是线程安全的。 我也非常非常想避免Thread.Sleep()。
到目前为止,我能做的最好的事情是:
long currentKey = 0;
ConcurrentDictionary timers = new ConcurrentDictionary();
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中的其他一些库,但我目前所缺少的,但是现在我能想到的唯一解决方案是修改 上面带有间隔运行的专用清理任务。 有什么建议吗?
Chuu asked 2020-08-10T16:26:14Z
12个解决方案
68 votes
我不知道您正在使用哪个版本的C#。 但是我认为您可以通过使用Task库来完成此任务。 然后看起来像这样。
public class PauseAndExecuter
{
public async Task Execute(Action action, int timeoutInMilliseconds)
{
await Task.Delay(timeoutInMilliseconds);
action();
}
}
treze answered 2020-08-10T16:26:20Z
26 votes
.Net 4没有内置功能可以很好地做到这一点。 Thread.Sleep甚至AutoResetEvent.WaitOne(timeout)都不好-它们会占用线程池资源,我已经被烧死了!
最轻巧的解决方案是使用计时器-特别是在您要执行许多任务的情况下。
首先创建一个简单的计划任务类:
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 _scheduledTasks = new ConcurrentDictionary();
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);
James Harcourt answered 2020-08-10T16:26:57Z
5 votes
我的例子:
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();
}
Nikolay Khilyuk answered 2020-08-10T16:27:17Z
4 votes
我使用此方法将任务计划为特定时间:
public void ScheduleExecute(Action action, DateTime ExecutionTime)
{
Task WaitTask = Task.Delay(ExecutionTime.Subtract(DateTime.Now));
WaitTask.ContinueWith(() => action());
WaitTask.Start();
}
应该注意的是,由于int32最大值,此方法只能工作约24天。
VoteCoffee answered 2020-08-10T16:27:41Z
2 votes
如果您不太关心时间的粒度,则可以创建一个计时器,该计时器每秒滴答一次,并检查需要在ThreadPool上排队的过期Action。 只需使用秒表类检查超时。
您可以使用当前的方法,除了“词典”将“秒表”作为其键并将“操作”作为其值。 然后,您只需对所有KeyValuePair进行迭代,找到到期的秒表,将Action排队,然后将其删除。 但是,您可以从LinkedList获得更好的性能和内存使用率(因为每次都将枚举整个对象,而删除项目则更加容易)。
Garo Yeriazarian answered 2020-08-10T16:28:06Z
2 votes
使用一键式计时器,您拥有的模型绝对是正确的选择。 您当然不想为其中每个创建一个新线程。 您可以有一个线程,并有一个按优先顺序排列的优先操作队列,但这没有必要的复杂性。
尽管我很想尝试一下,但在回调中调用Timeout.Infinite可能不是一个好主意。 我似乎回想起过去做过的事,效果还不错。 我承认,但这是一件很奇怪的事情。
您可以从集合中删除计时器,而不能丢弃它。 如果没有对该对象的引用,则可以进行垃圾回收,这意味着终结器将调用Timeout.Infinite方法。 只是没有您想要的及时。 但这不应该是一个问题。 您只是在短时间泄漏手柄。 只要您没有成千上万的此类物品长期处于闲置状态,就不会有问题。
另一种选择是让一个计时器队列保持分配状态,但是将其禁用(即,其超时和间隔设置为Timeout.Infinite)。 当需要计时器时,可以从队列中拉出一个计时器,进行设置,然后将其添加到您的收藏夹中。 超时到期后,您清除计时器并将其放回队列。 您可以根据需要动态地增加队列,甚至可以不时整理它。
这样可以防止您为每个事件泄漏一个计时器。 相反,您将拥有一个计时器池(非常类似于线程池,不是吗?)。
Jim Mischel answered 2020-08-10T16:28:46Z
1 votes
treze的代码运行正常。 这可能对那些必须使用旧版.NET的用户有所帮助:
private static volatile List _timers = new List();
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));
}
Koray answered 2020-08-10T16:29:06Z
1 votes
文档明确指出System.Timers.Timer具有AutoReset属性,仅针对您的要求:
[HTTPS://MSDN.Microsoft.com/恩-US/library/system.timers.timer.auto reset(V=vs.110).aspx]
Lu4 answered 2020-08-10T16:29:30Z
1 votes
这似乎对我有用。 它允许我在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));
naskew answered 2020-08-10T16:29:51Z
1 votes
使用Microsoft的Reactive Framework(NuGet“ System.Reactive”),然后可以执行以下操作:
protected void Execute(Action action, int timeout_ms)
{
Scheduler.Default.Schedule(TimeSpan.FromMilliseconds(timeout_ms), action);
}
Enigmativity answered 2020-08-10T16:30:10Z
0 votes
为什么不简单地在异步操作中调用操作参数本身呢?
Action timeoutMethod = () =>
{
Thread.Sleep(timeout_ms);
action();
};
timeoutMethod.BeginInvoke();
Tejs answered 2020-08-10T16:30:35Z
-2 votes
这可能会晚一点,但是这是我当前用于处理延迟执行的解决方案:
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))
Karsten answered 2020-08-10T16:30:59Z