kitii
kitii

Reputation: 13

C#/.NET Timers and the Win32 Sleep function are both inexact

For the following code:

The actual interval is always 1014.01 ms rather than 1000 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.

The System Clock of Windows 8 is exact, but both the .NET timers and the Sleep(int) function of Windows API are inexact.

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

Update:

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

References:

Source code of the ReactOS

Official Reference Source of the .NET 4.5 Update 1

The Shared Source CLI 2.0 (for native functions)

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

Thanks to everyone for help!

Upvotes: 1

Views: 2449

Answers (7)

Spencer Ruport
Spencer Ruport

Reputation: 35117

The Windows OS is simply not designed for such things. This is a slight drawback to any OS that supports context switching. If you need very precise timings you'll need to use an embedded system or an OS that is designed to behave this way.

There are methods that will certainly improve the timing accuracy of whatever behavior you're trying to produce but it will be unreliable at best. At the end of the day the operating system is free to force a context switch which could delay your timer at any time.

Wikipedia has some more info on the subject: http://en.wikipedia.org/wiki/Real-time_operating_system

Upvotes: 0

Black Frog
Black Frog

Reputation: 11713

If you need a Real-time Operating System, you need to look someplace else other than a Windows desktop OS.

For example: List of real-time operating systems

Upvotes: 1

Victor Zakharov
Victor Zakharov

Reputation: 26424

You should not rely on Timer/Sleep interval for time sensitive calculations - it will never be exact. You can use Ticks instead or other high precision techniques. Resolution of Ticks is 1ms on Windows 7, according to this answer.

Also see here for more information: How to make an accurate decimal Timer?

Upvotes: 0

Goran Štuc
Goran Štuc

Reputation: 581

why not use Stopwatch? it's very precise MSDN Stopwatch

Upvotes: 1

Your keyphrase is "multimedia timers".

Upvotes: 0

Tom Seddon
Tom Seddon

Reputation: 2748

You can call timeBeginPeriod to tighten up the timer resolution. This also affects GetTickCount.

See Why does increasing timer resolution via timeBeginPeriod impact power consumption? for a discussion of why you might not want to do this (no idea whether this would be a concern in your situation of course).

Upvotes: 1

Cory Nelson
Cory Nelson

Reputation: 29991

Sleep causes the OS to not schedule the thread until the time is up. Note that schedule != run.

Scheduling only adds the thread to a queue so it'll get run eventually, but not always immediately. For instance, if there's already a thread running, you still need to wait for its time slice to finish. If there are higher-priority threads in the queue, those could also run before it.

You should never count on Sleep() lasting exactly the amount of time you give it -- only at least that amount of time.

Timers basically operate the same way, but don't block a thread while they're waiting to be scheduled.

Also, you should be using Environment.TickCount or Stopwatch to measure elapsed time, not DateTime, which is affected by changes to the system time.

Upvotes: 1

Related Questions