Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a variant of `Task.Delay` that expires after real time passes e.g. even when the system is suspended and resumed?

I have a situation where it makes more sense to me to have a delay between periodic actions wait for an amount of time in the real world to pass rather than waiting for the system clock to tick some number of times. This way I could, say, renew a lease being tracked on a different system/being timed out in real time after some amount of real time passes.

I suspected that Task.Delay might already have this behavior, but I wanted to make sure, so I wrote a test program (see below). My discovery was that Task.Delay behaves quite differently when the system is suspended and resumed. From observing its behavior, Task.Delay acts as if it:

  • Sets a counter to the number of timer ticks necessary for this amount of time to pass.
  • Decrements that counter each time some timer ticks.
  • Marks itself as completed when the counter reaches 0.

Is there a way to await in such a way that I can run a task after some amount of real time passes so that if the system or process is resumed after the delay would have expired my continuation can be triggered? Right now, as a workaround, I’m just continuing whenever either Task.Delay expires or SystemEvents.PowerModeChanged fires Resume. Is this the correct way to handle the situation? It seems odd to me to have to compose two APIs intended for different purposes this way and I was surprised to see that SystemEvents.PowerModeChanged exists. Also, I fear that this API, being in the Microsoft.Win32 namespace, may not be portable.

Experiment

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);
        }
    }
}

In my program, I set task to a Task.Delay(TimeSpan.FromSeconds(20)). I then also print the current date once every second (plus a small amount of time) using a loop which runs 20 times (tickerTask).

The output for a system suspend resume is:

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

As you can see, I suspended my computer at 14:02:44 and resumed it at 14:03:03. Further, you can see that Task.Delay(TimeSpan.FromSeconds(20)) behaved roughly the same as looping 20 times over Task.Delay(TimeSpan.FromSeconds(1)). The total wait time of 38.9 seconds is roughly 20 seconds plus the sleep time of 18 seconds (03:03 minus 02:44). I was hoping that the total wait time would be the time prior to resume plus the sleep time: 28 seconds or 10 (02:44 minus 02:34) plus 18 seconds (03:03 minus 02:44).

When I use Process Explorer to suspend and resume the process, the Task.Delay() does faithfully complete after 20 seconds of real time. However, I am not certain that Process Explorer is actually suspending all of the threads of my process properly—maybe the message pump continues to run? Yet, the particular case of the process being suspended and resumed externally is both not really something most developers would try to support nor is it that different from normal process scheduling (which Task.Delay() is expected to handle).

like image 436
binki Avatar asked Jul 05 '16 15:07

binki


People also ask

What is a delayed task?

A delayed task starts as soon as it is eligible to run, and it finishes at a user-specified time. A started delayed-type task has a status of `In Progress` until it reaches its planned-for time, when it then changes its status to `Complete`.

Does task delay block?

Task. Delay() is asynchronous. It doesn't block the current thread. You can still do other operations within current thread.


1 Answers

A simple solution would be to write a method that periodically checks the current time and completes when difference from the start time reaches the desired amount:

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);
    }
}

What precision you should use depends on, well, the precision you require and the performance you need (though this likely isn't going to be a problem). If your delays are going to be in the range of seconds, then precision of hundreds of milliseconds sounds reasonable to me.

Note that this solution won't work correctly if the time on the computer changes (but DST transitions or other timezone changes are fine, since it's using UtcNow).

like image 150
svick Avatar answered Oct 29 '22 16:10

svick