1

对于以下代码:

实际间隔始终是 1014.01 ms 而不是 1000 ms ...

我也尝试在 C++ 中使用 System.Windows.Forms.Timer、System.Threading.Timer 和 WinAPI Sleep(int) 函数,但 14.01 毫秒的额外增加始终存在。

Windows 8 的系统时钟是精确的,但 .NET 计时器和 Windows API 的 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;
    }
}

更新:

  • 本机睡眠功能(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(用于本机功能)

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

感谢大家的帮助!

4

7 回答 7

1

睡眠会导致操作系统在时间到之前不调度线程。请注意 schedule != 运行。

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

你永远不应该指望 Sleep() 会持续你给它的时间——至少是那个时间。

计时器基本上以相同的方式运行,但在等待调度时不要阻塞线程。

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

于 2012-12-01T18:29:17.977 回答
1

你可以打电话timeBeginPeriod来收紧计时器的分辨率。这也影响GetTickCount.

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

于 2012-12-01T18:29:39.427 回答
1

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

于 2012-12-01T18:35:58.433 回答
1

如果您需要实时操作系统,则需要寻找 Windows 桌面操作系统以外的其他地方。

例如:实时操作系统列表

于 2012-12-01T18:45:48.863 回答
0

您不应该依赖 Timer/Sleep 间隔进行时间敏感计算 - 它永远不会准确。您可以改用Ticks或其他高精度技术。Ticks根据这个答案,Windows 7 上的分辨率为 1 毫秒。

另请参阅此处了解更多信息:如何制作准确的十进制计时器?

于 2012-12-01T18:28:04.570 回答
0

您的关键词是“多媒体计时器”。

于 2012-12-01T18:31:00.893 回答
0

Windows 操作系统根本不是为这些事情而设计的。这对于任何支持上下文切换的操作系统来说都是一个小缺点。如果您需要非常精确的计时,则需要使用嵌入式系统或旨在以这种方式运行的操作系统。

有一些方法肯定会提高你试图产生的任何行为的时间准确性,但它充其量是不可靠的。在一天结束时,操作系统可以自由地强制进行上下文切换,这可能会随时延迟您的计时器。

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

于 2012-12-01T19:50:39.307 回答