有没有`Task.Delay`的变体在实时通过后到期,例如即使系统被暂停和恢复?

bin*_*nki 8 .net c# delay task-parallel-library

我有一种情况,我觉得在现实世界中等待一段时间的周期性动作之间有一段延迟而不是等待系统时钟嘀嗒几次.通过这种方式,我可以更新在不同系统上跟踪的租约/在实际经过一段时间后实时超时.

我怀疑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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在我的程序中,我设置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
Run Code Online (Sandbox Code Playgroud)

如你所见,我在14:02:44暂停了我的电脑,并在14:03:03恢复了它.此外,您可以看到Task.Delay(TimeSpan.FromSeconds(20))与循环20次的行为大致相同Task.Delay(TimeSpan.FromSeconds(1)).总等待时间为38.9秒约为20秒加上18秒的休眠时间(03:03减去02:44).我希望总等待时间是恢复前加上睡眠时间的时间:28秒或10(02:44减去02:34)加上18秒(03:03减去02:44).

当我使用Process Explorer暂停和恢复该过程时,它Task.Delay()会在20秒实时后忠实完成.但是,我不确定Process Explorer是否实际上正在暂停我的进程的所有线程 - 也许消息泵继续运行?然而,流程被暂停和外部恢复的特定情况并不是大多数开发人员试图支持的,也不是与正常流程调度(Task.Delay()预期会处理)不同.

svi*_*ick 3

一个简单的解决方案是编写一个方法,定期检查当前时间,并在与开始时间的差异达到所需量时完成:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

您应该使用什么precision取决于您需要的精度和您需要的性能(尽管这可能不会成为问题)。如果您的延迟在几秒范围内,那么数百毫秒的精度对我来说听起来是合理的。

请注意,如果计算机上的时间发生变化,此解决方案将无法正常工作(但 DST 转换或其他时区更改都可以,因为它使用的是UtcNow)。