...

понедельник, 30 сентября 2013 г.

[Из песочницы] Timers in .Net

В последнее время не в первый раз сталкиваюсь с тем, что разработчики не до конца понимают как работает один из стандартных таймеров в .NET — System.Threading.Timer.

Т.е. в общем-то они вроде понимают что таймер что-то выполняет, скорее всего в ThreadPool — и если его использовать для периодического выполнения чего-либо, то он вполне подойдет. Но вот если вам надо создать не один таймер, а положим 1000, то тут люди начинают волноваться: а вдруг вот что-то там не так, а вдруг это все-таки 1000 потоков и даже боятся использовать их в таких случаях.

Хотелось бы пролить немного света на этот «таинственный» System.Threading.Timer.



В .NET еще существуют другие таймеры, но они в основном предназначены для решения специфических задач(например, для написания GUI приложений). Нами рассматриваемый предназначен для решения «системных» задач или использования в библиотеках.


Немного о том, как бы мы могли реализовать таймер.


Например, мы могли бы для каждой периодически выполняемой единицы работы создавать отдельный поток который просыпался бы по истечению определенного интервала времени, выполнял работу и засыпал опять. Понятно что это не самый лучший вариант.


Можно было бы пойти другим путем и использовать объект ядра «таймер». Для каждой периодической единицы работы создавать объект ядра и в отдельном потоке ожидать на них в стиле:



WaitHandle.WaitAny(/*timerHandles[]*/)




Но, к сожалению или нет, в .NET нет API для прямой работы с такими объектами(таймерами ядра).

Есть третий вариант реализации таймера(получившийся у разработчиков класса System.Threading.Timer)

При создании первого в домене приложения таймера через механизм P/Invoke создается объект ядра «таймер» это можно увидеть в классе System.Threading.TimerQueue:



[SecurityCritical]
[SuppressUnmanagedCodeSecurity]
[DllImport("QCall", CharSet = CharSet.Unicode)]
private static TimerQueue.AppDomainTimerSafeHandle CreateAppDomainTimer(uint dueTime);

// some code
if (this.m_appDomainTimer == null || this.m_appDomainTimer.IsInvalid)
{
this.m_appDomainTimer = TimerQueue.CreateAppDomainTimer(dueTime);
// some code


Также создается отдельный поток который высчитывает сколько надо подождать до ближайшего срабатывания одного из таймеров, устанавливает соответствующие параметры объекту ядра «таймер» и ждет.

Давайте посмотрим как это выглядит. Создадим консольный проект и подключим SOS Debugging Extension.


image


Как мы видим, перед созданием таймера у нас всего два потока: «основной» и поток «финализатора». Давайте продвинемся на одну строку ниже.


image


У нас появились два потока — один, ID 3, это как раз и есть поток который работает с объектом ядра «таймер». А второй, ID 4, это рабочий поток пула, он еще не успел запуститься, в нем будут исполняться наши callback.


Теперь как это все работает если вы последовательно создаете несколько таймеров

Возвращаемся к классу System.Threading.TimerQueue. Он является синглтоном. Каждый раз когда вы пишете код вида:



new Timer(First, null, 0, 250);




Это приводит к добавлению экземпляра класса System.Threading.TimerQueueTimer в его внутреннюю очередь(являющуюся чем-то вроде LinkedList). Т.е. этот класс содержит внутри себя все созданные таймеры(я склоняюсь что в рамках домена).

После того как первый таймер был создан. У TimerQueue будет регулярно вызыватьcя метод FireNextTimers.

Что он делает(код длинный, я не стал приводить исходники, кому интересно может посмотреть сам):

Он быстро пробегается по всем сохраненным в нем таймерам и находит время до ближайшего срабатывания таймера и настраивает объект ядра таймер на посылку нотификации через этот интервал. Как только эта нотификация будет получена, время следующего срабатывания будет пересчитано и объект ядра таймер будет настроен на новый интервал. При добавлении нового таймера время следующей нотификации будет пересчитано.

Давайте попробуем создать 1000 таймеров и посмотрим что из этого получится:


image


Мы видим, что создание 1000 таймеров не влечет за собой создание 1000 потоков. CLR создало один поток для работы с таймером ядра и несколько рабочих потоков для обработки срабатываний таймера.


Итого:

Когда вы работаете с классом System.Threading.Timer создается один(на домен приложения) объект ядра «таймер» и один поток для работы с ним который работает по принципу схожему с работой структуры данных «куча».

К вопросу о 1000 таймеров — накладно ли создавать такое количество таймеров в приложении, думаю что каждый конкретный случай надо рассматривать отдельно. Но знание того как устроены таймеры изнутри поможет принять правильное решение.


Испытывалось на Windows 7 64, .Net 4.5, VS2012.

Используемая литература: Duffy «Concurrent Programming on Windows», MSDN


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends:



Комментариев нет:

Отправить комментарий