суббота, 28 сентября 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);

Также создается отдельный поток который высчитывает сколько надо подождать до ближайшего срабатывания одного из таймеров, устанавливает соответствующие параметры объекту ядра "таймер" и ждет.
Давайте посмотрим как это выглядит. Создадим консольный проект и подключим SOS Debugging Extension.




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



У нас появились два потока - один, ID 3, это как раз и есть поток который работает с объектом ядра "таймер". А второй, ID 4, это рабочий поток пула, он еще не успел запуститься, в нем будут исполняться наши callback.
Теперь как это все работает если вы последовательно создаете несколько таймеров
Возвращаемся к классу System.Threading.TimerQueue.
Он является синглтоном. Каждый раз когда вы пишете код вида:
            new Timer(First, null, 0, 250);
Это приводит к добавлению экземпляра класса System.Threading.TimerQueueTimer в его внутреннюю очередь(являющуюся чем-то вроде LinkedList). Т.е. этот класс содержит внутри себя все созданные таймеры(я склоняюсь что в рамках домена).
После того как первый таймер был создан. У TimerQueue будет регулярно вызыватьcя метод FireNextTimers. Что он делает(код длинный, кому интересно может посмотреть исходники):
Он быстро пробегается по всем сохраненным в нем таймерам и находит время до ближайшего срабатывания таймера и настраивает объект ядра таймер на посылку нотификации через этот интервал. Как только эта нотификация будет получена, время следующего срабатывания будет пересчитано и объект ядра таймер будет настроен на новый интервал. При добавлении нового таймера время следующей нотификации будет пересчитано.
Давайте попробуем создать 1000 таймеров и посмотрим что из этого получится:


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

Итого:
Когда вы работаете с классом System.Threading.Timer создается один(на домен приложения) объект ядра "таймер" и один поток для работы с ним который работает по принципу схожему с работой структуры данных "куча".
К вопросу о 1000 таймеров - накладно ли создавать такое количество таймеров в приложении, думаю что каждый конкретный случай надо рассматривать отдельно. Но знание того как устроены таймеры изнутри поможет принять правильное решение.

Испытывалось на Windows 7 64, .Net 4.5, VS2012.
Используемая литература: Duffy Concurrent Programming on Windows, MSDN


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

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