有“任务”这个词的变体吗?在实时时间过去后过期的延迟,例如,即使系统被挂起和恢复



我有一种情况,对我来说,在周期性动作之间设置一个延迟,在现实世界中等待一定的时间,而不是等待系统时钟滴答几次,这更有意义。这样,我就可以在另一个系统上更新一个被跟踪的租约,或者在一段实时时间过去后实时超时。

我怀疑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);
        }
    }
}

在我的程序中,我设置taskTask.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在该实现中的使用。但我希望上述方法在许多情况下(如果不是大多数情况的话)都能奏效。

相关内容

最新更新