Is there a `Task.Delay` option that expires after passing real time, for example. even when the system is paused and resumed?

I have a situation where I have more sense so that the delay between periodic actions waits for time in the real world, rather than expecting the system clock to tick several times. Thus, I could, say, extend the lease, which is tracked in another system / real-time waiting time after passing a certain amount of time in real time.

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 very differently when the system is paused and resumed. Watching his behavior, Task.Delay acts like this:

  • Sets the counter of the number of timer ticks required for this amount of time.
  • Decreases the counter each time the timer goes off.
  • Marks itself complete when the counter reaches 0.

Is there an await way so that I can run the task after some real time has passed, so that if the system or process resumes after the delay expires, my continuation can be started? Right now, as a workaround, Im just continuing when either Task.Delay or Task.Delay ends with Resume firing. Is this the right way to handle the situation? It seems strange to me that I had to compose two APIs designed for different purposes, and I was surprised to see that SystemEvents.PowerModeChanged exists. In addition, I fear that this API, located 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 Task.Delay(TimeSpan.FromSeconds(20)) . Then I also print the current date once per second (plus a small amount of time), using a loop that runs 20 times ( tickerTask ).

Exit to resume the system is suspended:

 tick: 2016-07-05 AD 14:02:34 Started at 2016-07-05 AD 14:02:34, waiting 00:00:20. tick: 2016-07-05 AD 14:02:35 tick: 2016-07-05 AD 14:02:36 tick: 2016-07-05 AD 14:02:37 tick: 2016-07-05 AD 14:02:38 tick: 2016-07-05 AD 14:02:39 tick: 2016-07-05 AD 14:02:40 tick: 2016-07-05 AD 14:02:41 Microsoft.Win32.PowerModeChangedEventArgs: Suspend tick: 2016-07-05 AD 14:02:42 tick: 2016-07-05 AD 14:02:44 tick: 2016-07-05 AD 14:03:03 Microsoft.Win32.PowerModeChangedEventArgs: Resume tick: 2016-07-05 AD 14:03:05 tick: 2016-07-05 AD 14:03:06 tick: 2016-07-05 AD 14:03:08 tick: 2016-07-05 AD 14:03:09 tick: 2016-07-05 AD 14:03:10 tick: 2016-07-05 AD 14:03:11 tick: 2016-07-05 AD 14:03:12 Ended at 2016-07-05 AD 14:03:13, waited 00:00:38.8964427. tick: 2016-07-05 AD 14:03:13 tick: 2016-07-05 AD 14:03:14 

As you can see, I paused my computer at 14:02:44 and resumed it at 14:03:03. In addition, you can see that Task.Delay(TimeSpan.FromSeconds(20)) behaved in much the same way as the loop 20 times over Task.Delay(TimeSpan.FromSeconds(1)) . The total waiting time of 38.9 seconds is approximately 20 seconds plus a sleep time of 18 seconds (03:03 minus 02:44). I was hoping that the total wait time would be the time before resuming 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 pause and resume a process, Task.Delay() completes in good faith after 20 seconds of real time. However, I'm not sure if Process Explorer actually pauses all threads of my process - maybe the message pump continues to work? However, a special case of pausing and resuming the process from the outside is not something that most developers will try to support, and is no different from the usual process planning (which is expected to be handled by Task.Delay() ).

+7
c # task-parallel-library delay
source share
2 answers

A simple solution would be to write a method that periodically checks the current time and ends when the 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 the accuracy you need and the performance you need (although this probably won't be a problem). If your delays are in the range of seconds, then accuracy of hundreds of milliseconds will be reasonable for me.

Please note that this solution will not work correctly if the time on the computer changes (but DST transitions or other changes in the time zone are fine, as it uses UtcNow ).

+1
source share

it would be much preferable to have a โ€œzero costโ€ way to do this and be triggered by the event itself, rather than using a survey template

Ask and you will receive. Although almost a year later. :)

Inspired by another question, I spent some time today studying the processing options for timers in the context of a computer that goes into suspended power mode (i.e., sleep mode or sleep mode).

First, a summary. One commenter wrote:

Microsoft made this special choice because every single timer that exits instantly (regardless of their interval and their start time) when the OS resumes is much worse.

Perhaps Microsoft did, and perhaps they did not. The fact that for a long time the most common approach to timers was the WM_TIMER message. This is a โ€œsynthesizedโ€ message, meaning that it was generated at the moment when the message loop of the thread checks the messages if the timer has expired. This type of timer behaves exactly as the commentator describes it as "far, much worse."

Microsoft may have encountered problems and found out about their error. Or maybe it's not as bad as everyone, due to the relatively small number of timers that will usually be active at any given time. I dont know.

I know that due to the behavior of WM_TIMER one job is to use either System.Windows.Forms.Timer (from the Winforms API) or System.Windows.Threading.DispatcherTimer (from WPF). Both of these timer classes implicitly take into account pause / resume delays due to their synthesized behavior.

Other timer classes are not so successful. They rely on a Windows thread sleep mechanism that does not account for pause / resume delays. If you ask the thread to sleep, for example, for 10 seconds, then it will take 10 unoccupied seconds of the OS time before this thread wakes up again.

Then it goes over TPL, with Task.Delay() . And he, inside, uses the System.Threading.Timer class, which, of course, means that he does not take into account the suspension / resume delays.

But, you can build similar methods, except for those that take into account the suspended state. Here are some examples:

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

This allows the TPL API to do the job. IMHO, this is easier to read, but it introduces the need for a related CancellationTokenSource and uses exceptions (which are relatively heavy) to handle suspend / resume events.

Here's another version that indirectly uses the System.Threading.Timer class, because it is based on a timer class, which I wrote also based on this, but which uses suspend / resume events:

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

Here's the implementation for SleepAwareTimer , where the actual handling of the suspend / resume state is performed:

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

Here is a lot more code between the SleepAwareTimer and Delay() class. But the timer may restart after the system resumes, without throwing exceptions that may be considered useful.

Please note that in both implementations I try to unsubscribe from the SystemEvents.PowerModeChanged event. As a static event, failure to unsubscribe will cause a very permanent memory leak, as the event will infinitely bind to the subscriber link. This means that it is also important to place the SleepAwareTimer object; it makes no sense to use the finalizer to try to unsubscribe from the event, because the event will maintain the availability of the object, so the finalizer will never start. Thus, this object does not have a finalizer backup for code that does not delete the object!

The above were well prepared for me in my main tests. A more robust solution will probably start with the implementation of Task.Delay() in .NET and replace the use of System.Threading.Timer in this implementation with SleepAwareTimer shown above. But I would expect the above to work in many, if not most scenarios.

0
source share

All Articles