Я пишу приложение, которое должно будет использовать Timer, но потенциально очень много из них. Насколько масштабируемым является класс System.Threading.Timer? В документации просто сказано, что это «легкий», но не поясняется. Эти таймеры засасываются в один поток (или очень маленький пул потоков), который обрабатывает все обратные вызовы от имени Timer, или у каждого Timer есть свой собственный поток?
Думаю, еще один способ перефразировать вопрос: как реализован System.Threading.Timer?





Я думаю, вы можете переосмыслить свой дизайн (то есть, если вы сами контролируете дизайн). Если вы используете так много таймеров, что это действительно беспокоит вас, очевидно, что есть некоторый потенциал для консолидации.
Вот хорошая статья из журнала MSDN Magazine несколько лет назад, в которой сравниваются три доступных класса таймеров и дается некоторое представление об их реализациях:
Я говорю это в ответ на множество вопросов: не забывайте, что (управляемый) исходный код фреймворка доступен. Вы можете использовать этот инструмент, чтобы получить все это: http://www.codeplex.com/NetMassDownloader
К сожалению, в этом конкретном случае большая часть реализации находится в машинном коде, поэтому вы не можете на это смотреть ...
Однако они определенно используют потоки пула, а не таймер потока.
Стандартный способ реализовать большую коллекцию таймеров (именно так ядро делает это изнутри, и я подозреваю, что косвенно так и заканчивается ваша большая коллекция таймеров) состоит в том, чтобы поддерживать список, отсортированный по времени до истечения срока действия, поэтому система всегда должна беспокоиться только о проверке следующего таймера, срок действия которого истекает, а не всего списка.
Грубо говоря, это дает O (log n) для запуска таймера и O (1) для обработки запущенных таймеров.
Обновлено: только что искал в книге Джеффа Рихтера. Он говорит (о Threading.Timer), что он использует один поток для всех объектов Timer, этот поток знает, когда должен наступить следующий таймер (т.е. как указано выше), и вызывает ThreadPool.QueueUserWorkItem для обратных вызовов при необходимости. Это приводит к тому, что если вы не завершите обслуживание одного обратного вызова на таймере до истечения следующего срока, ваш обратный вызов повторно войдет в другой поток пула. Подводя итог, я сомневаюсь, что вы столкнетесь с большой проблемой при наличии большого количества таймеров, но вы можете столкнуться с исчерпанием пула потоков, если большое их количество запускается на одном таймере и / или их обратные вызовы работают медленно.
Конечно - «список, отсортированный по времени до истечения срока», безусловно, может быть своего рода приоритетной очередью - я не имел в виду «список, в котором была выполнена операция сортировки».
Я просто потратил некоторое время на просмотр кода в sscli. Обратите внимание, что .NET ThreadPool сильно изменился с момента выпуска Rotor, поэтому вполне возможно, что System.Threading.Timer также сильно изменился. На самом деле таймеры были сильно нарушены в .NET 1.1 и исправлены только для обеспечения безопасности и исключений в .NET 2.0. В любом случае, в Rotor таймеры хранятся в связанном списке и запускаются выделенным потоком запуска таймера. Существует один поток запуска таймера для всей среды выполнения (даже для нескольких доменов приложений).
Поток просто просматривает список таймеров и вызывает (собственный) QueueUserWorkItem для всех таймеров, срок действия которых истек. Операции вставки новых таймеров и удаления старых выполняются как вызовы APC в потоке таймера (msdn.microsoft.com/en-us/library/windows/desktop/…). Таким образом, реализация спроектирована так, чтобы быть простой и безопасной (сейчас), но не очень производительной. Тем не менее, это должно быть достаточно быстро для большинства ситуаций, но если вы пытаетесь создать 10000 таймеров в секунду, у вас, вероятно, возникнут проблемы.
Кстати, функция срабатывания таймера - это строка 3864 в пуле потоков win32.C++, если вам интересно.
На самом деле после небольшого теста выяснилось, что обход таймера по-прежнему O (log n), но даже все же мне удалось заставить таймеры идти в ногу со своей машиной со скоростью ~ 20000 в секунду.
^^ как говорит Дэнни Смурф: Объедините их. Создайте службу таймера и спросите у таймеров. Потребуется только сохранить 1 активный таймер (для следующего вызова) и историю всех запросов таймера и пересчитать это в AddTimer () / RemoveTimer ().
Consolidate them. Create a timer service and ask that for the timers. It will only need to keep 1 active timer (for the next due call)...
Чтобы это было улучшением по сравнению с простым созданием большого количества объектов Threading.Timer, вы должны предположить, что это не совсем то, что Threading.Timer уже делает внутри. Мне было бы интересно узнать, как вы пришли к такому выводу (я не разбирал нативные части фреймворка, так что вы вполне можете быть правы).
Очередь приоритетов, вероятно, будет более эффективной, чем отсортированный список, если все таймеры не будут добавлены сразу в начале, затем отсортированы и больше не будут добавлены позже.