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.