How timers works in the CLR
One of the coolest things about the CoreCLR being open sourced is that I can trawl through the source code and read random parts of the framework. One of the reasons to do this, is to be able to understand the implementation concerns, not just the design, which than allows us to produce a much better output.
In this case, I was investigating a hunch, and I found myself deep inside the code that runs the timers in .NET. The relevant code is here, and it is clearly commented as well as quite nice to read.
I’m just going to summarize a few interesting things I found in the code.
There is actually only one single real timer for the entire .NET process. I started out thinking this is handled via CreateTimerQueueTimer on Windows, but I couldn’t find a Linux implementation. Reading the code, the CLR actually implements this directly via this code. Simplified, it does the following:
This has some interesting implications. It means that timers are all going to be fired from the same thread, at the same time (not quite true, see below), and that there is likely going to be a problem with very long timers (a timer for three months from now will overflow int32, for example).
The list of timers is held in a linked list, and every time it is awakened, it runs through the list, finding the timer to trigger, and the next time to be triggered. The code in this code path is called with only a single timer, which is then used in the managed code for actually implementing the managed timers. It is important to note that actually running the timer callback is done by queuing that on the thread pool, not executing it on the timer thread.
On the managed side, there are some interesting comments explaining the expected usage and data structures used. There are two common cases, one is the use of timeout, in which case this is typically discarded before actual use, and the other is having the recurring timers, which tend to happen once in a long while. So the code favors adding / removing timers over actually finding which need to be executed.
Another thing to note is that this adding / removing / changing / iterating over timers is protected by a single lock. Every time the unmanaged timer wakes, it queue the callback on the thread pool, and then the FireNextTimers is called, which takes a look, iterates over all the timers, and queues all those timers to be executed on the thread pool.
This behavior is interesting, because it has some impact on commonly used cases. But I’ll discuss that on my next post.
Comments
:-)
50 days: http://www.google.com/search?q=4294967294+ms+in+days
I remember the following blog: http://www.catonmat.net/blog/settimeout-setinterval/
Javascript only 25 days :-)
Wow, I'm not sure I like that. Wouldn't this introduce timer drift?
Joseph, What would? And note that the timer explicitly doesn't give you promises on when you'll be called
I was looking at the .Net Reference Source you linked to and couldn't figure out some code. In a number of places in the code they do the following:
Does this have any purpose? I would expect code inside the try for it to do anything with exceptions. As far as I can tell, you can simple throw away the try/finally and just keep the code in the finally and it would all work 100% the same.
@Jeff:
The comment is short, but does give a clue. Thread aborts are odd on .NET; a ThreadAbortException will run finally blocks before the thread is actually aborted.
So, the code in the finally will run even if the thread is aborted. Where "aborted" is the interesting .NET "soft abort", not an actual TerminateThread, of course.
@Jeff - Putting the code in the finally block prevents it from being interrupted by a call to Thread.Abort.
You can see the difference in behavior in this code:
static void Main() { Thread t = new Thread(DoIt); t.Start(); t.Abort(); }
static void DoIt() { Console.WriteLine("a"); Thread.Sleep(1000); Console.WriteLine("b"); }
static void FinallyDoIt() { try { } finally { DoIt(); } Console.WriteLine("c"); }
If you call DoIt, "a" is printed, and then that thread is aborted before "b" is printed. OTOH, calling FinallyDoIt, the finally block is allowed to complete before the thread is aborted, so "a" and "b" are both printed, but "c" is not.
Thank you Stephan and Chris. You nailed it! I love learning new stuff. The ThreadAbortException didn't say very clearly what would happen if you are IN a finally when the abort happens.
For those at home who want to follow along, create a new C# console application and add the code below. I couldn't get Chris's code to work reliably without sticking in some
Thread.Sleep()
's in appropriate places.The
Thread.Sleep()
after thethread.Start()
is to allow the thread to actually get started before it is aborted. At least that is my take on it. TheThread.Sleep()
at the end ofMain()
is so you can see the results before the console app closes.Oren, you said timers are fired from the same thread. I assumed that meant the handlers executed in the same thread as the timer look, but I see that's not the case.
However, I see something else that could introduce drift. This line:
timerInfo->FiringTime = currentTime+timerInfo->Period;
I would have expected this, to keep the timer rock solid:
timerInfo->FiringTime += timerInfo->Period;
Because a non-trivial amount of time may pass between that line and
currentTime = GetTickCount();
above, especially when a timer ticks often.Hit post too soon, can't edit.
look, but
should bebut now as I look,
Wow, I really wasn't ready to hit post. Okay. Because
timerInfo->FiringTime
is set tocurrentTime+timerInfo->Period
instead oftimerInfo->FiringTime + timerInfo->Period
, the question is what happens whencurrentTime
andtimerInfo->FiringTime
are different? Slippage. What could cause them to be different?Thread.Sleep
might not get back to them exactly right away.This means that
FireTimers
runs when one timer is expired, but potentially a little later. It runs at timetimerInfo->FiringTime + sleepDelta
where sleepDelta is the amount of time later than planned that it is scheduled and runs. At timetimerInfo->FiringTime + sleepDelta
,currentTime = GetTickCount()
is taken. That meanscurrentTime = timerInfo->FiringTime + sleepDelta
. Then each time the timer does fire,timerInfo->FiringTime = currentTime+timerInfo->Period
; substituting currentTime,timerInfo->FiringTime = timerInfo->FiringTime + sleepDelta+timerInfo->Period
.As you said, there is no promise on when the timer's callback will be called, but it would be nice if the timer itself was drift-proof. I'm curious why the team didn't go with
timerInfo->FiringTime + timerInfo->Period
.Also, look at test1.c: SleepEx is only expected to be within 150 ms or 10% for longer intervals. It that's the spec, that's a possible 150 ms or 10% interval drift on every tick.
It sounds like the .net implementation is similar to a .Net timer wrapper I wrote. I wrote that so that I could make thousands of timers, and "pause" or destroy all of them simultaneously if necessary. However, now that you have commented on the inner workings of the .Net timer...I think I could simplify my wrapper code; the .Net implementation is already behaving mostly as I intended.
JJoseph, The CLR get the currentTime before running any operation, then return the minimum amount of time to wait from the start of the last run to the firing time. If there is slippage, it might actually happen early,not late. Note that we are waiting only until the first scheduled timer to run
Oren, you're right, I don't disagree about that. I explained it better in my second comment. The problem is that due to the thread sleep,
currentTime
is later thantimerInfo->FiringTime
at the linecurrentTime = GetTickCount();
.Later, rather than setting the next fire time to the previous fire time + the interval, it's set to the current time + the interval. Since the current time is later than the previous fire time, the timer can drift and only in one direction.
Comment preview