Accuracy of Task.Delay

2019-06-17 04:49发布

问题:

I'm developing Windows 10 Universal App in C#/Xaml, I'm using await Task.Delay(delayInMilliseconds) to suspend a method for given time. My scenario is somewhat realtime, so it's very sensitive to time changes, and I need to make sure that when i suspend a method for let's say 8 millisecods it is suspended for 8 millisecods. I have noticed that the actual time span for which ** Task.Delay ** suspends the method differes from what is passed as delay parameter, for 1 up to 30 ms, the length of "deviation" being different in each call. So, when I want to sleep for 8 milliseconds, my system sleeps for 9 to 39 milliseconds, and this completly ruins my scenario. So, my question what is the best way to substitute ** Task.Delay ** and to achieve good precision? For the time being I use this method:

 public static void Delay1(int delay)
    {
        long mt = delay * TimeSpan.TicksPerMillisecond;
        Stopwatch s = Stopwatch.StarNew();
        while (true)
        {
            if (s.Elapsed.TotalMilliseconds > delay)
            {
                return;
            }
        }
    }

but it guees it consumes a lot of resources, that is 100% of a processor's core. If an user has small number of processor's cores, this would be very insufficient.

回答1:

According to msdn it's not possible to achieve more accuracy due to system clock resolution:

This method depends on the system clock. This means that the time delay will approximately equal the resolution of the system clock if the millisecondsDelay argument is less than the resolution of the system clock, which is approximately 15 milliseconds on Windows systems.



回答2:

You should use multi-media timers. Those are much accurate. Take a look here: https://social.msdn.microsoft.com/Forums/vstudio/en-US/2024e360-d45f-42a1-b818-da40f7d4264c/accurate-timer



回答3:

Seems like I've found a sattisfactory solution:

new System.Threading.ManualResetEvent(false).WaitOne(delayInMilliseconds) instead of await Task.Delay(delayInMilliseconds);

I've used following code to test both methods:

async void Probe()
    {
        for (int i = 0; i < 1000; i++)
        {
            // METHOD 1
            await Task.Delay(3); 
           // METHOD 2
           new System.Threading.ManualResetEvent(false).WaitOne(3); 
         }
    }

The code above should take exactly 3 seconds to execute. With METHOD 2 it took around 3300 ms, so the eror is 0.3 ms per call. Acceptable for me. But the METHOD 1 tok around 15 seconds (!) to execute which gives totally unacceptable error for my scenario. I only wonder what's the usage of CPU with Method 2, hope it doesn't use polling, the documentation says something about use of signals but unfortnatelly it doesn't automatically mean that polling isn't used spomewhere else.



回答4:

After working hard I found a solution to sleep the thread for specific time and the error is just 3 to 4 microseconds on my Core 2 Duo CPU 3.00 GHz. Here it is:

Here is code.(C# code is also given at the end.) Use "ThreadSleepHandler":

Public Class ThreadSleepHandler
Private Stopwatch As New Stopwatch
Private SpinStopwatch As New Stopwatch
Private Average As New TimeSpan
Private Spinner As Threading.SpinWait
Public Sub Sleep(Time As TimeSpan)
    Stopwatch.Restart()
    If Average.Ticks = 0 Then Average = GetSpinTime()
    While Stopwatch.Elapsed < Time
        If Stopwatch.Elapsed + Average < Time Then
            Average = TimeSpan.FromTicks((Average + GetSpinTime()).Ticks / 2)
        End If
    End While
End Sub
Public Function GetSpinTime() As TimeSpan
    SpinStopwatch.Restart()
    Spinner.SpinOnce()
    SpinStopwatch.Stop()
    Return SpinStopwatch.Elapsed
End Function
End Class

Here is example code:

Sub Main()
    Dim handler As New ThreadSleepHandler
    Dim stopwatch As New Stopwatch
    Do
        stopwatch.Restart()
        handler.Sleep(TimeSpan.FromSeconds(1))
        stopwatch.Stop()
        Console.WriteLine(stopwatch.Elapsed)
    Loop
End Sub

For c# programmers here is code (I have converted this code but I am not sure):

static class Main
{
public static void Main()
{
    ThreadSleepHandler handler = new ThreadSleepHandler();
    Stopwatch stopwatch = new Stopwatch();
    do {
        stopwatch.Restart();
        handler.Sleep(TimeSpan.FromSeconds(1));
        stopwatch.Stop();
        Console.WriteLine(stopwatch.Elapsed);
    } while (true);
}
}
public class ThreadSleepHandler
{
private Stopwatch Stopwatch = new Stopwatch();
private Stopwatch SpinStopwatch = new Stopwatch();
private TimeSpan Average = new TimeSpan();
private Threading.SpinWait Spinner;
public void Sleep(TimeSpan Time)
{
    Stopwatch.Restart();
    if (Average.Ticks == 0)
        Average = GetSpinTime();
    while (Stopwatch.Elapsed < Time) {
        if (Stopwatch.Elapsed + Average < Time) {
            Average = TimeSpan.FromTicks((Average + GetSpinTime()).Ticks / 2);
        }
    }
}
public TimeSpan GetSpinTime()
{
    SpinStopwatch.Restart();
    Spinner.SpinOnce();
    SpinStopwatch.Stop();
    return SpinStopwatch.Elapsed;
}
}

Note: "ThreadSleepHandler" is thread unsafe. You cannot use a single "ThreadSleepHandler" for multiple threads.

The first sleep time will not be enough accurate.



回答5:

In my tests, I found that DispatcherTimer at 20ms intervals will deliver sufficiently smooth animation if you must do it in code. Doing it in XAML is another alternative as already mentioned.