Questions:
Why is the System.Threading.Timer
keeping the 15ms resolution despite the OS clock resolution is much more precise?
What is the recommendable way to achieve 1ms timing events resolution without busy CPU waiting?
To stress once more: System timer has 1ms resolution in my case (as opposed to the question suggested as duplicate). So this is not an issue of system timer resolution. Therefore, there is no useful info in the supposedly duplicate question.
Background:
It seems that .NET System.Threading.Timer
is not using system clock resolution - it keeps the ~ 15ms resolution. Despite the OS clock (and e.g. Sleep
resolution) is much more precise.
On my box (when almost idle and 4 cores are available to run):
>Clockres.exe
ClockRes v2.0 - View the system clock resolution
Copyright (C) 2009 Mark Russinovich
SysInternals - www.sysinternals.com
Maximum timer interval: 15.625 ms
Minimum timer interval: 0.500 ms
Current timer interval: 1.001 ms
Output of my quick test:
Sleep test:
Average time delta: 2[ms] (from 993 cases)
System.Threading.Timer test:
Average time delta: 15[ms] (from 985 cases)
Where the test code is:
private static void TestSleepVsTimer(long millisecondsDifference, int repetions)
{
TimingEventsKeeper timingEventsKeeper = new TimingEventsKeeper();
timingEventsKeeper.Reset((int) millisecondsDifference, repetions);
while (!timingEventsKeeper.TestDoneEvent.IsSet)
{
timingEventsKeeper.CountNextEvent(null);
Thread.Sleep((int) millisecondsDifference);
}
Console.WriteLine("Sleep test: ");
timingEventsKeeper.Output();
timingEventsKeeper.Reset((int) millisecondsDifference, repetions);
Timer t = new Timer(timingEventsKeeper.CountNextEvent, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1));
timingEventsKeeper.TestDoneEvent.Wait();
Console.WriteLine("System.Threading.Timer test: ");
timingEventsKeeper.Output();
}
private class TimingEventsKeeper
{
long _ticksSum = 0;
long _casesCount = 0;
long _minTicksDiff;
long _maxTicksDiff;
long _lastTicksCount;
int _repetitons;
public CountdownEvent TestDoneEvent = new CountdownEvent(0);
public void Reset(int millisecondsDifference, int repetitions)
{
_ticksSum = 0;
_casesCount = 0;
_minTicksDiff = millisecondsDifference * 10000;
_maxTicksDiff = millisecondsDifference * 10000;
_lastTicksCount = DateTime.UtcNow.Ticks;
_repetitons = repetitions;
TestDoneEvent.Reset(repetitions);
}
public void CountNextEvent(object unused)
{
long currTicksCount = DateTime.UtcNow.Ticks;
long diff = currTicksCount - _lastTicksCount;
_lastTicksCount = currTicksCount;
TestDoneEvent.Signal();
if (diff >= _maxTicksDiff)
{
_maxTicksDiff = diff;
return;
}
if (diff <= _minTicksDiff)
{
_minTicksDiff = diff;
return;
}
_casesCount++;
_ticksSum += diff;
}
public void Output()
{
if(_casesCount > 0)
Console.WriteLine("Average time delta: {0}[ms] (from {1} cases)", _ticksSum / _casesCount / 10000, _casesCount);
else
Console.WriteLine("No measured cases to calculate average");
}
}
public static class WinApi
{
/// <summary>TimeBeginPeriod(). See the Windows API documentation for details.</summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Interoperability", "CA1401:PInvokesShouldNotBeVisible"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2118:ReviewSuppressUnmanagedCodeSecurityUsage"), SuppressUnmanagedCodeSecurity]
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", SetLastError = true)]
public static extern uint TimeBeginPeriod(uint uMilliseconds);
/// <summary>TimeEndPeriod(). See the Windows API documentation for details.</summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Interoperability", "CA1401:PInvokesShouldNotBeVisible"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2118:ReviewSuppressUnmanagedCodeSecurityUsage"), SuppressUnmanagedCodeSecurity]
[DllImport("winmm.dll", EntryPoint = "timeEndPeriod", SetLastError = true)]
public static extern uint TimeEndPeriod(uint uMilliseconds);
}
private static void Main(string[] args)
{
WinApi.TimeBeginPeriod(1);
TestSleepVsTimer(1, 1000);
WinApi.TimeEndPeriod(1);
}
EDIT1:
Environment:
Tested on Build and Release version under .NET 2.0, 3.0, 3.5 (without CountDownEvent) and 4.5
On Windows 8 (Build 9200), Server 2012 (Build 9200), Server 2008 (Build 6001 SP1)
Everywhere with significant difference between Sleep
and Timer
.
Why this is not duplicate:
As I posted - the OS timer resolution is set to 1ms (and also Sleep
doesn't exhibit the behavior). Therefore this is not fault of OS timer resolution (interrupts frequency) - this is something specific to System.Threading.Timer
.
EDIT2:
(Added TimeBeginPeriod
and TimeEndPeriod
calls to code - to force OS timer resolution change)
System.Threading.Timer
limits you to 15 ms is a bit of a mystery. But that's the way it is. I gave up on trying to figure out why and just wrote my own interface to the Windows Timer Queue timers. See Using the Windows Timer Queue API. – QktpWin32WaitOrTimerCallback
is passed to the unmanaged code, but it actually creates a reference likenew delegate
and it is nowhere stored, and thus gets wiped at first GC run. Then when the timer ticks, exception happens. I added a field in the timer class and stored the reference, it fixed the problem. Letting you know here, because the blog post doesn't have comments. – Ratliff