Пользователь
0,0
рейтинг
30 сентября 2013 в 21:08

Разработка → Timers in .Net из песочницы

C#*, .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
карма
16,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (23)

  • +1
    А теперь в примере с двумя таймерами соберём мусор после создания таймеров и… Ничего не выводится, т.к. на таймеры нет ссылок и сборщик мусора их любезно убрал.
    • 0
      Да и в примере с 1000 таймеров тоже нет ссылок и сборщик мусора любезно убирает весь список со всеми таймерами.
      • +1
        Так как проект запущен в режиме «Debug» то все эти переменные будут «живы» до выхода из метода.
        • +2
          Это понятно, что в дебаге они будут живы, но не редки случаи, когда люди смотрят, что в дебаге работает, компилят в релиз и получают тыкву.
        • +2
          Мне кажется правильно было бы сделать об этом пометку в тексте статьи. Те, кто благодаря вашей статье наконец-то разберутся, как же работает таймер и что это такое, скорее всего очень удивятся подобному поведению GC.
  • 0
    А можно ли с помощью таймера ядра работать с отсечками менее 15 миллисекунд?
    • 0
      Windows — не ОСРВ, поэтому вообще проблематично что-либо гарантировать.
      Ну а на тему таймеров была уже хорошая статья.
      • 0
        Ещё есть документик от Microsoft на тему таймеров.
      • 0
        А например в WinCE это будет работать, или там для этого нужно другие механизмы использовать?
        • 0
          Если использовать хардварный таймер, а не софтовый, то вполне можно добиться высокой точность. А если софтовый таймер, то надо учитывать, что всегда есть задержки.
          Ещё есть хорошее сравнение разных систем.
  • +1
    Это приводит к добавлению экземпляра класса System.Threading.TimerQueueTimer в его внутреннюю очередь(являющуюся чем-то вроде LinkedList).
    Вот именно это-то момент и является для меня самым интересным — какую структуру данных они используют. Была бы гарантия, что там нормальная куча — использовал бы их, а так пришлось писать свой велосипед.
    • +1
      А чем вас не устроил LinkedList?
      • +1
        Там не используется LinkedList, способ внутреннего хранения таймеров схож с работой LinkedList. Раз такой интерес к этому способу хранения, я допишу об этом позже.
        • +1
          Подождите, здесь явно какое-то более интересная интрига! Ждём ответа автора корневого комментария!
      • +1
        Тем, что на десяти тысячах таймеров он будет тормозить.
        • +1
          Насколько я понимаю, при реализации такого механизма таймеров, планировщику нужно после каждого срабатывания таймера последовательно и полностью пробегаться по коллекции и оценивать хранящиеся там значения интервалов. Во всех типах коллекций последовательный перебор работает более-менее одинаково же…
          • 0
            Это только до тех пор, пока коллекция неупорядоченная — в упорядоченной не требуется последовательного перебора. А в древовидных еще и вставка с удалением работают быстрее (чем в упорядоченных списках).
            • 0
              Искать в неупорядоченном списке на 10К элементов — действительно гиблое дело. Но кто сказал что реализация очереди таймеров опирается на поиск?
              • 0
                Никто не говорил. Более того, я не понял, что вы сейчас сказали.
                • 0
                  Я лишь пытаюсь понять, зачем было изобретать собственный велосипед.
                  • –1
                    Чтобы работал быстрее, я же сказал об этом.
  • 0
    Недавно заметил такую особенность: когда создаешь объект System.Timers.Timer и запускаешь его, то все в порядке, пока веб сервер, в домене приложения которого создан таймер, не обрабатывает большое количество запросов. Если же веб-сервер находиться под нагрузкой (например нагрузочное тестирование с 500 одновременных подключений/запросов) то таймер перестает генерировать событие Elapsed. с чем это может быть связано?
  • 0
    Событие Elapsed генерируется в потоке пула. Если потоки пула оказались все заняты запросами и в итоге закончились — событие Elapsed будет ждать в очереди.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.