有没有`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的使用。 但我希望上述内容可以在许多情况下使用,即使不是大多数情况。