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 then 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 and 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 it is typically discarded before actual use, and the other is 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 queues 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.