C#/。NET定时器和Win32睡眠function都是不精确的

对于下面的代码:

实际的间隔总是1014.01毫秒,而不是1000毫秒…

我也试过在C ++中使用System.Windows.Forms.Timer,System.Threading.Timer和WinAPI Sleep(int)函数,但是总是会增加14.01 ms。

Windows 8的系统时钟是确切的,但Windows API的.NET定时器和Sleep(int)函数都是不精确的。

public partial class Form1 : Form { private long ticks; public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { System.Timers.Timer timer = new System.Timers.Timer(1000); // The actual interval is always 1014.01 ms ... // I've also tried to use System.Windows.Forms.Timer, System.Threading.Timer // and the WinAPI Sleep(int) function in C++, but the additional increase // of 14.01 ms always exists. timer.Elapsed += timer_Elapsed; timer.Start(); ticks = System.DateTime.Now.Ticks; } void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { textBox1.Text = Math.Round((e.SignalTime.Ticks - ticks) / 10000.0, 2).ToString(); ticks = e.SignalTime.Ticks; } } 

更新:

  • 本地睡眠function(ReactOS):
 // Call SleepEx with bAlertable = FALSE VOID WINAPI Kernel32.Sleep(IN DWORD dwMilliseconds) // Call NtDelayExecution with Alertable = bAlertable // and DelayInterval.QuadPart = dwMilliseconds * -10,000 DWORD WINAPI Kernel32.SleepEx(IN DWORD dwMilliseconds, IN BOOL bAlertable) // The syscall stub - call the kernel mode function NtDelayExecution directly NTSTATUS NTAPI Ntdll.NtDelayExecution(IN BOOLEAN Alertable, IN PLARGE_INTEGER DelayInterval) // Check for the access permissions of DelayInterval and then call KeDelayExecutionThread NTSYSCALLAPI NTSTATUS NTAPI Ntoskrnl.NtDelayExecution(IN BOOLEAN Alertable, IN PLARGE_INTEGER DelayInterval) // Core implement of the sleep/delay function NTKERNELAPI NTSTATUS NTAPI Ntoskrnl.KeDelayExecutionThread(IN KPROCESSOR_MODE WaitMode, IN BOOLEAN Alertable, IN PLARGE_INTEGER Interval OPTIONAL) { PKTIMER Timer; PKWAIT_BLOCK TimerBlock; PKTHREAD Thread = KeGetCurrentThread(); NTSTATUS WaitStatus; BOOLEAN Swappable; PLARGE_INTEGER OriginalDueTime; LARGE_INTEGER DueTime, NewDueTime, InterruptTime; ULONG Hand = 0; /* If this is a user-mode wait of 0 seconds, yield execution */ if (!(Interval->QuadPart) && (WaitMode != KernelMode)) { /* Make sure the wait isn't alertable or interrupting an APC */ if (!(Alertable) && !(Thread->ApcState.UserApcPending)) { /* Yield execution */ NtYieldExecution(); } } /* Setup the original time and timer/wait blocks */ OriginalDueTime = Interval; Timer = &Thread->Timer; TimerBlock = &Thread->WaitBlock[TIMER_WAIT_BLOCK]; /* Check if the lock is already held */ if (!Thread->WaitNext) goto WaitStart; /* Otherwise, we already have the lock, so initialize the wait */ Thread->WaitNext = FALSE; KxDelayThreadWait(); /* Start wait loop */ for (;;) { /* Disable pre-emption */ Thread->Preempted = FALSE; /* Check if a kernel APC is pending and we're below APC_LEVEL */ if ((Thread->ApcState.KernelApcPending) && !(Thread->SpecialApcDisable) && (Thread->WaitIrql < APC_LEVEL)) { /* Unlock the dispatcher */ KiReleaseDispatcherLock(Thread->WaitIrql); } else { /* Check if we have to bail out due to an alerted state */ WaitStatus = KiCheckAlertability(Thread, Alertable, WaitMode); if (WaitStatus != STATUS_WAIT_0) break; /* Check if the timer expired */ InterruptTime.QuadPart = KeQueryInterruptTime(); if ((ULONGLONG)InterruptTime.QuadPart >= Timer->DueTime.QuadPart) { /* It did, so we don't need to wait */ goto NoWait; } /* It didn't, so activate it */ Timer->Header.Inserted = TRUE; /* Handle Kernel Queues */ if (Thread->Queue) KiActivateWaiterQueue(Thread->Queue); /* Setup the wait information */ Thread->State = Waiting; /* Add the thread to the wait list */ KiAddThreadToWaitList(Thread, Swappable); /* Insert the timer and swap the thread */ ASSERT(Thread->WaitIrql <= DISPATCH_LEVEL); KiSetThreadSwapBusy(Thread); KxInsertTimer(Timer, Hand); WaitStatus = (NTSTATUS)KiSwapThread(Thread, KeGetCurrentPrcb()); /* Check if were swapped ok */ if (WaitStatus != STATUS_KERNEL_APC) { /* This is a good thing */ if (WaitStatus == STATUS_TIMEOUT) WaitStatus = STATUS_SUCCESS; /* Return Status */ return WaitStatus; } /* Recalculate due times */ Interval = KiRecalculateDueTime(OriginalDueTime, &DueTime, &NewDueTime); } WaitStart: /* Setup a new wait */ Thread->WaitIrql = KeRaiseIrqlToSynchLevel(); KxDelayThreadWait(); KiAcquireDispatcherLockAtDpcLevel(); } /* We're done! */ KiReleaseDispatcherLock(Thread->WaitIrql); return WaitStatus; NoWait: /* There was nothing to wait for. Did we have a wait interval? */ if (!Interval->QuadPart) { /* Unlock the dispatcher and do a yield */ KiReleaseDispatcherLock(Thread->WaitIrql); return NtYieldExecution(); } /* Unlock the dispatcher and adjust the quantum for a no-wait */ KiReleaseDispatcherLockFromDpcLevel(); KiAdjustQuantumThread(Thread); return STATUS_SUCCESS; } // Note that the Windows API Sleep(0) will also call NtYieldExecution(), refer to // the function Ntoskrnl.KeDelayExecutionThread above 
  • .NET Sleep(1),Sleep(0),Yield()和empty语句的超时:
 for (; ; ) { Stopwatch sw = Stopwatch.StartNew(); // Thread.Sleep(1); // between 36000 and 39000 // Thread.Sleep(0); // 2 or 3 Thread.Yield(); // 1 or 2 // empty statement // always 0 Console.WriteLine(sw.ElapsedTicks); sw.Restart(); } 
  • 秒表依赖于WinAPI函数QueryPerformanceCounter和QueryPerformanceFrequency:
 static Stopwatch() { bool succeeded = SafeNativeMethods.QueryPerformanceFrequency(out Frequency); if(!succeeded) { IsHighResolution = false; Frequency = TicksPerSecond; tickFrequency = 1; } else { IsHighResolution = true; tickFrequency = TicksPerSecond; tickFrequency /= Frequency; } } public static long GetTimestamp() { if(IsHighResolution) { long timestamp = 0; SafeNativeMethods.QueryPerformanceCounter(out timestamp); return timestamp; } else { return DateTime.UtcNow.Ticks; } } 
  • 秒表是确切的,但既不DateTime.UtcNow.Ticks也不是Environment.TickCount是确切的:
 // Stopwatch is extremely exact without Thread.Sleep, always 1000.00 ms // But the combination of Stopwatch + Thread.Sleep(1000) is inexact // Stopwatch is very exact with Thread.Sleep + a spin check, always 1000 ms thread = new Thread(() => { var setText = new Action<long>(t => textBox1.Text = Math.Round(t * 1000.0 / Stopwatch.Frequency, 2).ToString()); var sw = Stopwatch.StartNew(); for (; ; ) { // In most cases 986 is exact enough, but very rarely it might produce // a "1001", so use 985 here Thread.Sleep(985); while (sw.ElapsedTicks < Stopwatch.Frequency) // Use Sleep(0) instead of Yield() or empty statement Thread.Sleep(0); // The actual interval is always 1000 ms instead of 1014.01 ms // The Invoke method must be used since InvokeRequired is true Invoke(setText, sw.ElapsedTicks); sw.Restart(); } }); thread.Start(); // DateTime.UtcNow.Ticks and DateTime.Now.Ticks are both inexact with // Thread.Sleep + a spin check, still 1014.01 ms thread = new Thread(() => { var setText = new Action<long>(t => textBox1.Text = Math.Round((t - ticks) / 10000.0, 2).ToString()); for (; ; ) { Thread.Sleep(985); while (DateTime.UtcNow.Ticks < ticks + 10000000) Thread.Sleep(0); var t = DateTime.UtcNow.Ticks; Invoke(setText, t); ticks = t; } }); thread.Start(); // Environment.TickCount is inexact with Thread.Sleep + a spin check, // still 1014 ms (int value) thread = new Thread(() => { var setText = new Action<int>(t => textBox1.Text = (t - msecs).ToString()); for (; ; ) { Thread.Sleep(985); while (Environment.TickCount < msecs + 1000) Thread.Sleep(0); var t = Environment.TickCount; Invoke(setText, t); msecs = t; } }); thread.Start(); private void Form1_FormClosed(object sender, FormClosedEventArgs e) { thread.Abort(); } 

参考文献:

ReactOS的源代码

.NET 4.5 Update 1的官方参考资料来源

共享源CLI 2.0(用于本机function)

SwitchToThread / Thread.Yield与Thread.Sleep(0)与Thead.Sleep(1)

感谢大家的帮助!

休眠会导致操作系统不安排线程,直到时间到了。 请注意,schedule!=运行。

调度只将线程添加到队列中,以便最终运行,但不总是立即运行。 例如,如果已经有一个线程正在运行,您仍然需要等待其时间片完成。 如果队列中有更高优先级的线程,那么它们也可以在它之前运行。

你永远不要指望Sleep()持续的时间恰好是你给它的时间量 – 至少只有这个时间量。

定时器的运行方式基本相同,但在等待排定时不要阻塞线程。

此外,您应该使用Environment.TickCountStopwatch来测量已用时间,而不是DateTime ,这受到系统时间更改的影响。

您可以调用timeBeginPeriod来加强计时器分辨率。 这也影响GetTickCount

请参阅为什么通过timeBeginPeriod增加定时器分辨率会影响功耗? 讨论为什么你可能不想这样做(当然不知道这是否会成为你关心的问题)。

为什么不使用秒表? 这是非常精确的MSDN秒表

如果您需要实时操作系统,则除了Windows桌面操作系统之外,您还需要查看其他位置。

例如: 实时操作系统列表

您的主要关键词是“多媒体定时器”。

您不应该依赖计时器/睡眠间隔进行时间敏感的计算 – 这绝不会是精确的。 您可以使用Ticks或其他高精度技术。 根据这个答案 ,在Windows 7上, Ticks分辨率是1ms。

在这里也可以看到更多的信息: 如何制作一个精确的小数点定时器?

Windows操作系统根本不是为这样的事情设计的。 这对任何支持上下文切换的操作系统都是一个小缺点。 如果您需要非常精确的时间安排,则需要使用嵌入式系统或设计用于执行此操作的操作系统。

有一些方法肯定会提高你想要产生的任何行为的时间精度,但最好是不可靠的。 在一天结束时,操作系统可以自由地强制上下文切换,这可能会随时延迟您的计时器。

维基百科有关于这个主题的更多信息: http : //en.wikipedia.org/wiki/Real-time_operating_system