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

China☆狼群 提交于 2020-01-03 08:58:05

问题


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).


回答1:


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).




回答2:


it would be much preferable to have a “zero cost” way to do it and be triggered by the event itself rather than using a polling pattern

Ask and ye shall receive. Albeit almost a year later. :)

Inspired by a different question, I spent some time today exploring options for handling timers in the context of a computer that is going into a suspended power mode (i.e. sleep or hibernate).

First, a recap. A commenter above wrote:

Microsoft made this particular choice because every single timer instantly completing (regardless of their Interval and the time they got started) when the OS is resumed is far, far worse.

Maybe Microsoft did, and maybe they didn't. Fact is, for a long time, the most common approach for timers was the WM_TIMER message. This is a "synthesized" message, meaning it's generated at the exact moment a thread's message loop checks for messages, if the timer has expired. This type of timer behaves exactly as the commenter describes as "far, far worse".

Maybe Microsoft ran into problems with this and learned from their mistake. Or maybe it's not really as bad as all that, due to the relative small number of timers that would normally be active at any given time. I don't know.

What I do know is that, due to the behavior of WM_TIMER, one work-around 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 suspend/resume delays, due to their synthesized behavior.

Other timer classes aren't so lucky. They rely on Windows' thread sleep mechanism, which doesn't take into account suspend/resume delays. If you ask a thread to sleep, for example, for 10 seconds, then it will take 10 unsuspended seconds of OS time before that thread is woken up again.

Then comes along the TPL, with Task.Delay(). And it, internally, uses the System.Threading.Timer class, which of course means it has the same lack of taking into account of suspend/resume delays.

But, it is possible to construct similar methods, except which take into account a suspended state. Here are a couple of 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 one recomposes the TPL API to do the work. IMHO, this is easier to read, but it does introduce the need for a linked CancellationTokenSource, and uses exceptions (which are relatively heavy-weight) to deal with suspend/resume events.

Here's a different version, one which indirectly uses the System.Threading.Timer class, because it's based on a timer class I wrote also based on that, but which uses the 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, which is where the actual handling of the suspend/resume state is done:

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

There's a lot more code here, between the SleepAwareTimer class and the Delay() method. But the timer is able to restart itself after a system resume without throwing an exceptions, which might be considered beneficial.

Note that in both implementations, I'm careful to unsubscribe from the SystemEvents.PowerModeChanged event. Being a static event, failing to unsubscribe will cause a very persistent memory leak, because the event would hang on to a reference to the subscriber indefinitely. This means it's also important to dispose the SleepAwareTimer object; there's no point in using a finalizer to try to unsubscribe from the event, because the event will keep the object reachable, so the finalizer would never run. So this object has no finalizer backup for code that fails to dispose the object!

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



来源:https://stackoverflow.com/questions/38207026/is-there-a-variant-of-task-delay-that-expires-after-real-time-passes-e-g-even

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!