Recently, not for the first time I come across the fact that developers do not fully understand how one of the standard timers in .NET - System.Threading.Timer works.
Those. in general, they seem to understand that the timer does something, most likely in a ThreadPool - and if you use it to periodically execute something, it will work fine. But if you need to create not one timer, but set 1000, then people start to worry: what if something is wrong there, and what if it is still 1000 threads and even afraid to use them in such cases.
I would like to shed some light on this "mysterious" System.Threading.Timer.
In .NET, there are still other timers, but they are mainly intended for solving specific problems (for example, for writing GUI applications). We consider is designed to solve "system" problems or use in libraries.
A little bit about how we could implement a timer.
')
For example, for each periodically performed unit of work, we could create a separate thread that would wake up after a certain time interval, perform work, and fall asleep again. It is clear that this is not the best option.
One could go a different way and use the “timer” kernel object. For each periodic unit of work, create a core object and, in a separate thread, expect them in the style of:
WaitHandle.WaitAny()
But, unfortunately or not, in .NET there is no API for working directly with such objects (kernel timers).
There is a third variant of the timer implementation (obtained from the developers of the class System.Threading.Timer)
When creating the first timer application in the domain through the P / Invoke mechanism, a “timer” core object is created in the System.Threading.TimerQueue class:
[SecurityCritical] [SuppressUnmanagedCodeSecurity] [DllImport("QCall", CharSet = CharSet.Unicode)] private static TimerQueue.AppDomainTimerSafeHandle CreateAppDomainTimer(uint dueTime);
It also creates a separate thread that calculates how much you need to wait until the closest triggering of one of the timers, sets the appropriate parameters to the “timer” kernel object and waits.
Let's see how it looks. Create a console project and connect the SOS Debugging Extension.
As we can see, before creating the timer, we have only two streams: the “main” and the “finalizer”. Let's move one line down.
We have two threads - one, ID 3, this is exactly the thread that works with the “timer” core object. And the second, ID 4, is the workflow of the pool, it has not yet managed to start, our callbacks will be executed in it.
Now how it all works if you create several timers in sequence
Go back to the System.Threading.TimerQueue class. He is a singleton. Every time you write code like this:
new Timer(First, null, 0, 250);
This results in adding an instance of the System.Threading.TimerQueueTimer class to its internal queue (which is something like LinkedList). Those. this class contains all the created timers within itself (I incline that within the domain).
After the first timer has been created. The TimerQueue will regularly call the FireNextTimers method.
What does it do (the code is long, I did not cite the source code, for whom it may be interesting to see for myself):
He quickly goes over all the timers stored in it and finds the time until the closest triggering of the timer and sets the kernel object timer to send a notification through this interval. As soon as this notification is received, the next trigger time will be recalculated and the kernel object timer will be set to a new interval. When adding a new timer, the time of the next notification will be recalculated.
Let's try creating 1000 timers and see what happens:
We see that creating 1000 timers does not entail creating 1000 threads. The CLR created one thread to work with a kernel timer and several worker threads to handle timer responses.
Total:
When you work with the System.Threading.Timer class, there is one (per application domain) kernel object “timer” and one thread for working with it, which works on the principle similar to the work of the heap data structure.
On the question of 1000 timers - is it expensive to create so many timers in the application, I think that each case should be considered separately. But knowing how timers are built from the inside will help make the right decision.
Tested on Windows 7 64, .Net 4.5, VS2012.
References: Duffy "Concurrent Programming on Windows", MSDN