Точность 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 является еще одной альтернативой, как уже упоминалось.