有没有`Task.Delay`的变体在实时通过后到期,例如即使系统被暂停和恢复?
我有一种情况,我觉得在现实世界中等待一段时间的周期性动作之间有一段延迟,而不是等待系统时钟嘀嗒几次。 通过这种方式,我可以更新在不同系统上跟踪的租约/在实际经过一段时间后实时超时。
我怀疑Task.Delay
可能已经有这种行为,但我想确定,所以我写了一个测试程序(见下文)。 我的发现是,当系统挂起并恢复时, Task.Delay
行为完全不同。 从观察其行为开始, Task.Delay
就像它一样:
- 将计数器设置为通过此时间量所需的计时器滴答数。
- 每次计时器滴答时计数器减少。
- 当计数器达到0时,标记为已完成。
有没有办法以这样一种方式await
我可以在经过一定量的实时后运行任务,这样如果系统或进程在延迟过期后恢复,我的继续可以被触发? 现在,作为一种解决方法,只要Task.Delay
过期或SystemEvents.PowerModeChanged
触发Resume
,我就会Resume
。 这是处理这种情况的正确方法吗? 对于我来说,用这种方式编写两个用于不同目的的API似乎很奇怪,我惊讶地发现SystemEvents.PowerModeChanged
存在。 另外,我担心这个API在Microsoft.Win32
命名空间中可能不是可移植的。
实验
using Microsoft.Win32; using System; using System.Threading.Tasks; class Program { static int Main(string[] args) => new Program().Run(args).Result; async Task Run(string[] args) { SystemEvents.PowerModeChanged += (sender, e) => Console.WriteLine($"{e}: {e.Mode}"); var targetTimeSpan = TimeSpan.FromSeconds(20); var start = DateTime.UtcNow; var task = Task.Delay(targetTimeSpan); var tickerTask = Tick(targetTimeSpan); Console.WriteLine($"Started at {start}, waiting {targetTimeSpan}."); await task; var end = DateTime.UtcNow; Console.WriteLine($"Ended at {end}, waited {end - start}."); await tickerTask; return 0; } async Task Tick(TimeSpan remaining) { while (remaining > TimeSpan.Zero) { Console.WriteLine($"tick: {DateTime.UtcNow}"); await Task.Delay(TimeSpan.FromSeconds(1)); remaining -= TimeSpan.FromSeconds(1); } } }
在我的程序中,我将task
设置为Task.Delay(TimeSpan.FromSeconds(20))
。 然后我还使用循环运行20次( tickerTask
)每秒打印一次当前日期(加上少量时间)。
系统暂停恢复的输出是:
tick: 2016-07-05 AD 14:02:34 Started at 2016-07-05 AD 14:02:34, waiting 00:00:20. tick: 2016-07-05 AD 14:02:35 tick: 2016-07-05 AD 14:02:36 tick: 2016-07-05 AD 14:02:37 tick: 2016-07-05 AD 14:02:38 tick: 2016-07-05 AD 14:02:39 tick: 2016-07-05 AD 14:02:40 tick: 2016-07-05 AD 14:02:41 Microsoft.Win32.PowerModeChangedEventArgs: Suspend tick: 2016-07-05 AD 14:02:42 tick: 2016-07-05 AD 14:02:44 tick: 2016-07-05 AD 14:03:03 Microsoft.Win32.PowerModeChangedEventArgs: Resume tick: 2016-07-05 AD 14:03:05 tick: 2016-07-05 AD 14:03:06 tick: 2016-07-05 AD 14:03:08 tick: 2016-07-05 AD 14:03:09 tick: 2016-07-05 AD 14:03:10 tick: 2016-07-05 AD 14:03:11 tick: 2016-07-05 AD 14:03:12 Ended at 2016-07-05 AD 14:03:13, waited 00:00:38.8964427. tick: 2016-07-05 AD 14:03:13 tick: 2016-07-05 AD 14:03:14
如您所见,我在14:02:44暂停了我的电脑,并在14:03:03恢复了它。 此外,您可以看到Task.Delay(TimeSpan.FromSeconds(20))
行为与在Task.Delay(TimeSpan.FromSeconds(1))
循环20次的行为大致相同。 总等待时间为38.9秒约为20秒加上18秒的hibernate时间(03:03减去02:44)。 我希望总等待时间是恢复前加上睡眠时间的时间:28秒或10秒(02:44减去02:34)再加上18秒(03:03减去02:44)。
当我使用Process Explorer暂停和恢复进程时, Task.Delay()
会在20秒实时后忠实完成。 但是,我不确定Process Explorer是否实际上正在暂停我的进程的所有线程 – 也许消息泵继续运行? 然而,暂停和恢复外部进程的特定情况并不是大多数开发人员试图支持的,也不是与正常进程调度( Task.Delay()
预期要处理的不同)。
一个简单的解决方案是编写一个定期检查当前时间的方法,并在与开始时间的差异达到所需数量时完成:
public static Task RealTimeDelay(TimeSpan delay) => RealTimeDelay(delay, TimeSpan.FromMilliseconds(100)); public static async Task RealTimeDelay(TimeSpan delay, TimeSpan precision) { DateTime start = DateTime.UtcNow; DateTime end = start + delay; while (DateTime.UtcNow < end) { await Task.Delay(precision); } }
您应该使用的precision
取决于您所需的精度和所需的性能(尽管这可能不会成为问题)。 如果您的延迟将在几秒钟的范围内,那么几百毫秒的精度对我来说听起来是合理的。
请注意,如果计算机上的时间发生变化,此解决方案将无法正常工作(但DST转换或其他时区更改很好,因为它使用的是UtcNow
)。
最好采用“零成本”的方式来实现它并由事件本身触发而不是使用轮询模式
问,你们会收到。 虽然差不多一年之后。 🙂
受到另一个问题的启发,我今天花了一些时间探索在进入暂停电源模式(即睡眠或hibernate)的计算机环境中处理计时器的选项。
首先,回顾一下。 一位评论者写道:
微软做出了这个特别的选择,因为当操作系统恢复时,每个单独的计时器立即完成(无论他们的间隔和开始时间)都要差得多。
也许微软做了,也许他们没有。 事实上,很长一段时间,定时器最常用的方法是WM_TIMER
消息。 这是一个“合成”消息,这意味着它是在线程的消息循环检查消息的确切时刻生成的,如果计时器已过期。 这种类型的计时器的行为与评论者所描述的“远,更糟糕”完全相同。
也许微软遇到了这个问题并从他们的错误中吸取教训。 或者它可能并不像所有那样糟糕,因为在任何给定时间通常都会活动的定时器数量相对较少。 我不知道。
我所知道的是,由于WM_TIMER
的行为,一种解决方法是使用System.Windows.Forms.Timer
(来自Winforms API)或System.Windows.Threading.DispatcherTimer
(来自WPF)。 由于它们的合成行为,这两个计时器类隐含地考虑了暂停/恢复延迟。
其他计时器类没那么幸运。 它们依赖于Windows的线程hibernate机制,它没有考虑暂停/恢复延迟。 如果你要求一个线程hibernate,例如10秒钟,那么在该线程再次被唤醒之前需要10个未挂起的OS时间秒。
然后是TPL,带有Task.Delay()
。 它在内部使用System.Threading.Timer
类,这当然意味着它同样缺乏考虑暂停/恢复延迟。
但是,有可能构建类似的方法,除了考虑暂停状态。 以下是几个例子:
public static Task Delay(TimeSpan delay) { return Delay(delay, CancellationToken.None); } public static async Task Delay(TimeSpan delay, CancellationToken cancelToken) { CancellationTokenSource localToken = new CancellationTokenSource(), linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, localToken.Token); DateTime delayExpires = DateTime.UtcNow + delay; PowerModeChangedEventHandler handler = (sender, e) => { if (e.Mode == PowerModes.Resume) { CancellationTokenSource oldSource = localToken, oldLinked = linkedSource; localToken = new CancellationTokenSource(); linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, localToken.Token); oldSource.Cancel(); linkedSource.Dispose(); } }; SystemEvents.PowerModeChanged += handler; try { while (delay > TimeSpan.Zero) { try { await Task.Delay(delay, linkedSource.Token); } catch (OperationCanceledException) { cancelToken.ThrowIfCancellationRequested(); } delay = delayExpires - DateTime.UtcNow; } } finally { linkedSource.Dispose(); SystemEvents.PowerModeChanged -= handler; } }
这个重新组合TPL API来完成工作。 恕我直言,这更容易阅读,但它确实引入了对链接的CancellationTokenSource
,并使用exception(相对较重)处理暂停/恢复事件。
这是一个不同的版本,间接使用System.Threading.Timer
类,因为它基于我写的计时器类,但它使用挂起/恢复事件:
public static Task Delay(TimeSpan delay, CancellationToken cancelToken) { // Possible optimizations if (cancelToken.IsCancellationRequested) { return Task.FromCanceled(cancelToken); } if (delay <= TimeSpan.Zero) { return Task.CompletedTask; } return _Delay(delay, cancelToken); } private static async Task _Delay(TimeSpan delay, CancellationToken cancelToken) { // Actual implementation TaskCompletionSource taskSource = new TaskCompletionSource (); SleepAwareTimer timer = new SleepAwareTimer( o => taskSource.TrySetResult(true), null, TimeSpan.FromMilliseconds(-1), TimeSpan.FromMilliseconds(-1)); IDisposable registration = cancelToken.Register( () => taskSource.TrySetCanceled(cancelToken), false); timer.Change(delay, TimeSpan.FromMilliseconds(-1)); try { await taskSource.Task; } finally { timer.Dispose(); registration.Dispose(); } }
这是SleepAwareTimer
的实现,它是挂起/恢复状态的实际处理完成的地方:
class SleepAwareTimer : IDisposable { private readonly Timer _timer; private TimeSpan _dueTime; private TimeSpan _period; private DateTime _nextTick; private bool _resuming; public SleepAwareTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) { _dueTime = dueTime; _period = period; _nextTick = DateTime.UtcNow + dueTime; SystemEvents.PowerModeChanged += _OnPowerModeChanged; _timer = new System.Threading.Timer(o => { _nextTick = DateTime.UtcNow + _period; if (_resuming) { _timer.Change(_period, _period); _resuming = false; } callback(o); }, state, dueTime, period); } private void _OnPowerModeChanged(object sender, PowerModeChangedEventArgs e) { if (e.Mode == PowerModes.Resume) { TimeSpan dueTime = _nextTick - DateTime.UtcNow; if (dueTime < TimeSpan.Zero) { dueTime = TimeSpan.Zero; } _timer.Change(dueTime, _period); _resuming = true; } } public void Change(TimeSpan dueTime, TimeSpan period) { _dueTime = dueTime; _period = period; _nextTick = DateTime.UtcNow + _dueTime; _resuming = false; _timer.Change(dueTime, period); } public void Dispose() { SystemEvents.PowerModeChanged -= _OnPowerModeChanged; _timer.Dispose(); } }
SleepAwareTimer
类和Delay()
方法之间有很多代码。 但是计时器能够在系统恢复后重新启动而不会抛出exception,这可能被认为是有益的。
请注意,在这两种实现中,我都要小心取消订阅SystemEvents.PowerModeChanged
事件。 作为static
事件,未能取消订阅将导致非常持久的内存泄漏,因为事件将无限期地挂起到订阅者的引用。 这意味着配置SleepAwareTimer
对象也很重要; 使用终结器尝试取消订阅事件是没有意义的,因为事件将保持对象可达,因此终结器永远不会运行。 因此,对于无法处理对象的代码,此对象没有终结器备份!
在我的基本测试中,以上对我来说效果很好。 更强大的解决方案可能从.NET中的Task.Delay()
实现开始,并使用上面显示的SleepAwareTimer
替换该实现中System.Threading.Timer
的使用。 但我希望上述内容可以在许多情况下使用,即使不是大多数情况。