System.Timers.Timer массово неточно

Я написал программу, которая использует все доступные ядра, используя Parallel.ForEach. Список для ForEach содержит ~ 1000 объектов и вычисление для каждого объекта занимает некоторое время (~ 10 секунд). В этом случае я настраиваю таймер следующим образом:

timer = new System.Timers.Timer();
timer.Elapsed += TimerHandler;
timer.Interval = 15000;
timer.Enabled = true;

private void TimerHandler(object source, ElapsedEventArgs e)
{
        Console.WriteLine(DateTime.Now + ": Timer fired");
}

В настоящий момент метод TimerHandler является заглушкой, чтобы убедиться, что проблема не вызвана этим методом.

Я ожидал, что метод TimerHandler будет выполняться каждые ~ 15 секунд. Однако время между двумя вызовами этого метода достигает даже 40 секунд, поэтому слишком много секунд. Используя new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount -1 } для метода Parallel.ForEach, этого не происходит, и ожидается ожидаемый интервал в 15 секунд.

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

Изменить: как указано в настройке Юваля, фиксированный минимум потоков в пуле на ThreadPool.SetMinThreads решил проблему. Я также пробовал new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount } (поэтому без -1 в начальном вопросе) для метода Parallel.ForEach, и это также решает проблему. Однако у меня нет хорошего объяснения, почему эти модификации решили проблему. Возможно, было создано столько потоков, что поток таймера только что "потерялся" для "длительного" времени, пока он не будет выполнен снова.

Ответ 1

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

Класс Parallel подвержен возникновению безумных задач без ограничений. Легко спровоцировать его буквально порождать неограниченные потоки (2 в секунду). Я считаю класс Parallel непригодным для использования без указанного вручную max-DOP. Это бомба замедленного действия, которая случайно взрывается в производстве под нагрузкой.

Parallel особенно ядовит в сценариях ASP.NET, где многие запросы используют один пул потоков.

Обновление: Я забыл сделать ключевой момент. Таймеры тикера помещаются в очередь на пул потоков. Если пул насыщен, они попадают в линию и выполняются позднее. (Это причина, по которой таймеры могут происходить одновременно или после остановки таймеров.) Это объясняет, что вы видите. Способ исправить это, чтобы исправить перегрузку пула.

Лучшим решением для этого конкретного сценария будет настраиваемый планировщик задач с фиксированным количеством потоков. Parallel можно использовать для использования планировщика задач. У Parallel Extension Extras есть такой планировщик. Получите эту работу из общего потока потоков. Как правило, я бы recomment PLINQ, но это не в состоянии принять планировщик. В некотором смысле, как Parallel, так и PLINQ являются бесполезно искалеченными API.

Не используйте ThreadPool.SetMinThreads. Не связывайтесь с глобальными настройками процесса. Просто оставьте бедный пул нитей.

Кроме того, не используйте Environment.ProcessorCount -1, потому что это отбрасывает одно ядро.

таймер уже выполнен в собственном потоке

Таймер - это структура данных в ядре ОС. Нет нити, пока галочка не будет поставлена ​​в очередь. Не уверен, как именно это работает, но тик в конечном итоге помещается в пул потоков в .NET. Вот когда проблема начинается.

В качестве решения вы можете запустить поток, который спит в цикле, чтобы имитировать таймер. Это взлом, хотя, потому что он не устраняет основную причину: перегруженный пул потоков.