Timers in .Net

в 17:08, , рубрики: .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

Автор: f0bos

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js