Точность Task.Delay

Я разрабатываю универсальное приложение для Windows 10 на C#/Xaml, я использую await Task.Delay (delayInMilliseconds), чтобы приостановить метод на определенное время. Мой сценарий несколько в реальном времени, поэтому он очень чувствителен к изменениям времени, и мне нужно убедиться, что при приостановке метода, скажем, 8 миллисекод, он приостанавливается на 8 миллисекод. Я заметил, что фактический промежуток времени, для которого ** Task.Delay ** приостанавливает метод, отличается от того, что передается в качестве параметра задержки, в течение от 1 до 30 мс, причем длина "отклонения" различна в каждом вызове. Поэтому, когда я хочу спать в течение 8 миллисекунд, моя система спит от 9 до 39 миллисекунд, и это полностью разрушает мой сценарий. Итак, мой вопрос, каков наилучший способ заменить ** Task.Delay ** и добиться хорошей точности? В настоящее время я использую этот метод:

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

но он предполагает, что он потребляет много ресурсов, то есть 100% ядра процессора. Если у пользователя небольшое количество ядер процессора, этого будет очень мало.

5 ответов

Согласно msdn, невозможно добиться большей точности из-за разрешения системных часов:

Этот метод зависит от системных часов. Это означает, что временная задержка будет приблизительно равна разрешению системных часов, если аргумент millisecondsDelay меньше, чем разрешение системных часов, которое в системах Windows составляет приблизительно 15 миллисекунд.

Похоже, я нашел удовлетворительное решение:

новый System.Threading.ManualResetEvent(false).WaitOne(delayInMilliseconds) вместо await Task.Delay(delayInMilliseconds);

Я использовал следующий код для проверки обоих методов:

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

Код выше должен занять ровно 3 секунды для выполнения. С METHOD 2 это заняло около 3300 мс, поэтому ошибка составляет 0,3 мс на вызов. Приемлемо для меня. Но МЕТОД 1 потребует около 15 секунд (!) Для выполнения, что дает совершенно неприемлемую ошибку для моего сценария. Мне только интересно, как используется ЦП в методе 2, надеюсь, что он не использует опрос, документация что-то говорит об использовании сигналов, но, к сожалению, это не означает, что опрос не используется где-то еще.

Вы должны использовать мультимедийные таймеры. Это очень точно. Посмотрите здесь: https://social.msdn.microsoft.com/Forums/vstudio/en-US/2024e360-d45f-42a1-b818-da40f7d4264c/accurate-timer

После усердной работы я нашел решение перевести нить в режим ожидания на определенное время, и ошибка составляет всего 3-4 микросекунды на моем процессоре Core 2 Duo 3,00 ГГц. Вот: введите описание изображения здесь

Вот код.(Код C# также приводится в конце.) Используйте "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

Вот пример кода:

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

Для программистов на C# вот код (я преобразовал этот код, но я не уверен):

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

Примечание: ThreadSleepHandler является небезопасным потоком. Вы не можете использовать один ThreadSleepHandler для нескольких потоков.

Первое время сна не будет достаточно точным.

В своих тестах я обнаружил, что DispatcherTimer с интервалами в 20 мсек обеспечивает достаточно плавную анимацию, если вы должны сделать это в коде. Выполнение этого в XAML является еще одной альтернативой, как уже упоминалось.

Другие вопросы по тегам