有“任务”这个词的变体吗?在实时时间过去后过期的延迟,例如,即使系统被挂起和恢复
本文关键字:例如 延迟 过期 恢复 挂起 系统 过去 实时 任务 时间 | 更新日期: 2023-09-27 17:54:52
我有一种情况,对我来说,在周期性动作之间设置一个延迟,在现实世界中等待一定的时间,而不是等待系统时钟滴答几次,这更有意义。这样,我就可以在另一个系统上更新一个被跟踪的租约,或者在一段实时时间过去后实时超时。
我怀疑Task.Delay
可能已经有这种行为了,但是我想确定一下,所以我写了一个测试程序(见下文)。我的发现是,当系统挂起和恢复时,Task.Delay
的行为非常不同。从观察它的行为来看,Task.Delay
的行为就像:
- 将计数器设置为该时间通过所需的计时器刻度数。
- 每次计时器滴答时计数器的减数。
- 当计数器达到0时,标记自己已完成。
是否有一种方法可以await
这样一种方式,我可以运行一个任务后,一定数量的实时通过,以便如果系统或进程在延迟过期后恢复,我的延续可以被触发?现在,作为一种变通办法,每当Task.Delay
到期或SystemEvents.PowerModeChanged
触发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<int> 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 A.D. 14:02:34
Started at 2016-07-05 A.D. 14:02:34, waiting 00:00:20.
tick: 2016-07-05 A.D. 14:02:35
tick: 2016-07-05 A.D. 14:02:36
tick: 2016-07-05 A.D. 14:02:37
tick: 2016-07-05 A.D. 14:02:38
tick: 2016-07-05 A.D. 14:02:39
tick: 2016-07-05 A.D. 14:02:40
tick: 2016-07-05 A.D. 14:02:41
Microsoft.Win32.PowerModeChangedEventArgs: Suspend
tick: 2016-07-05 A.D. 14:02:42
tick: 2016-07-05 A.D. 14:02:44
tick: 2016-07-05 A.D. 14:03:03
Microsoft.Win32.PowerModeChangedEventArgs: Resume
tick: 2016-07-05 A.D. 14:03:05
tick: 2016-07-05 A.D. 14:03:06
tick: 2016-07-05 A.D. 14:03:08
tick: 2016-07-05 A.D. 14:03:09
tick: 2016-07-05 A.D. 14:03:10
tick: 2016-07-05 A.D. 14:03:11
tick: 2016-07-05 A.D. 14:03:12
Ended at 2016-07-05 A.D. 14:03:13, waited 00:00:38.8964427.
tick: 2016-07-05 A.D. 14:03:13
tick: 2016-07-05 A.D. 14:03:14
你可以看到,我在14:02:44暂停了我的电脑,并在14:03:03重新启动了它。此外,您可以看到Task.Delay(TimeSpan.FromSeconds(20))
的行为与在Task.Delay(TimeSpan.FromSeconds(1))
上循环20次大致相同。38.9秒的总等待时间大约是20秒加上18秒的睡眠时间(03:03减去02:44)。我希望总等待时间是恢复前的时间加上睡眠时间:28秒或10(02:44减去02:34)加上18秒(03:03减去02:44)。
当我使用进程资源管理器挂起和恢复进程时,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
)。
最好采用一种"零成本"的方式来完成它,并由事件本身触发,而不是使用轮询模式
请求就会得到。尽管差不多一年之后。:)
受到另一个问题的启发,我今天花了一些时间探索在计算机进入暂停电源模式(即睡眠或休眠)的情况下处理定时器的选项。
首先回顾一下。上面的一位评论者写道:
微软做了这个特殊的选择,因为当操作系统恢复时,每个计时器立即完成(不管它们的间隔和开始的时间)是非常非常糟糕的。
也许微软有,也许他们没有。事实上,很长一段时间以来,计时器最常用的方法是WM_TIMER
消息。这是一个"合成"消息,这意味着它是在线程的消息循环检查消息的确切时刻生成的,如果计时器已经过期。这种类型的计时器的行为就像评论者所描述的"远,远更糟糕"。
也许微软遇到了问题,并从他们的错误中吸取了教训。也可能情况并没有那么糟糕,因为在任何给定的时间,通常都有相对较少的计时器处于活动状态。我不知道。
我所知道的是,由于WM_TIMER
的行为,一个解决方案是使用System.Windows.Forms.Timer
(来自Winforms API)或System.Windows.Threading.DispatcherTimer
(来自WPF)。由于它们的合成行为,这两个计时器类都隐式地考虑了挂起/恢复延迟。
其他定时器类就没有这么幸运了。它们依赖于Windows的线程睡眠机制,该机制不考虑挂起/恢复延迟。例如,如果你要求一个线程休眠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来完成这项工作。IMHO,这更容易阅读,但它确实引入了对链接CancellationTokenSource
的需求,并使用异常(相对较重)来处理挂起/恢复事件。
这里是一个不同的版本,一个间接使用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<bool> taskSource = new TaskCompletionSource<bool>();
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()
方法之间。但是计时器能够在系统恢复后重新启动自己而不会抛出异常,这可能被认为是有益的。
注意,在这两个实现中,我都小心地取消了对SystemEvents.PowerModeChanged
事件的订阅。作为static
事件,未能取消订阅将导致非常持久的内存泄漏,因为该事件将无限期地挂起对订阅者的引用。这意味着处理SleepAwareTimer
对象也很重要;使用终结器来尝试取消对事件的订阅是没有意义的,因为事件将使对象保持可访问性,因此终结器永远不会运行。所以这个对象没有终止器备份来处理处理对象失败的代码!
在我的基本测试中,上述方法对我很有效。一个更健壮的解决方案可能是从。net中的Task.Delay()
实现开始,并用上面所示的SleepAwareTimer
取代System.Threading.Timer
在该实现中的使用。但我希望上述方法在许多情况下(如果不是大多数情况的话)都能奏效。