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).
See Question&Answers more detail:
os