System.Threading.Timer в С #, похоже, не работает. Он работает очень быстро каждые 3 секунды

112

У меня есть объект таймера. Я хочу, чтобы он запускался каждую минуту. В частности, он должен запускать OnCallBackметод и становиться неактивным во время выполнения OnCallBackметода. После OnCallBackзавершения метода он (а OnCallBack) перезапускает таймер.

Вот что у меня есть прямо сейчас:

private static Timer timer;

private static void Main()
{
    timer = new Timer(_ => OnCallBack(), null, 0, 1000 * 10); //every 10 seconds
    Console.ReadLine();
}

private static void OnCallBack()
{
    timer.Change(Timeout.Infinite, Timeout.Infinite); //stops the timer
    Thread.Sleep(3000); //doing some long operation
    timer.Change(0, 1000 * 10);  //restarts the timer
}

Однако, похоже, это не работает. Он работает очень быстро каждые 3 секунды. Даже если поднять точку (1000 * 10). Вроде закрывает глаза на1000 * 10

Что я сделал не так?

Алан Коромано
источник
12
From Timer.Change: «Если dueTime равен нулю (0), метод обратного вызова вызывается немедленно.». Похоже, для меня это ноль.
Damien_The_Unbeliever
2
Да, но что с того? тоже есть период.
Алан Коромано,
10
Так что, если есть еще период? Цитируемое предложение не претендует на значение периода. Он просто говорит: «Если это значение равно нулю, я немедленно вызову обратный вызов».
Damien_The_Unbeliever
3
Интересно, что если вы установите для обоих dueTime и period значение 0, таймер будет запускаться каждую секунду и запускаться немедленно.
Кельвин

Ответы:

231

Это неправильное использование System.Threading.Timer. Когда вы создаете экземпляр таймера, вы почти всегда должны делать следующее:

_timer = new Timer( Callback, null, TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite );

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

private void Callback( Object state )
{
    // Long running operation
   _timer.Change( TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite );
}

Таким образом, нет необходимости в механизмах блокировки, потому что нет параллелизма. Таймер запустит следующий обратный вызов по истечении следующего интервала + время длительной операции.

Если вам нужно запустить таймер с точностью до N миллисекунд, я предлагаю вам измерить время длительной операции с помощью секундомера, а затем соответствующим образом вызвать метод Change:

private void Callback( Object state )
{
   Stopwatch watch = new Stopwatch();

   watch.Start();
   // Long running operation

   _timer.Change( Math.Max( 0, TIME_INTERVAL_IN_MILLISECONDS - watch.ElapsedMilliseconds ), Timeout.Infinite );
}

Я настоятельно рекомендую всем, кто занимается .NET и использует CLR, кто не читал книгу Джеффри Рихтера - CLR через C # , прочитать как можно скорее. Там очень подробно описаны таймеры и пулы потоков.

Иван Златанов
источник
6
Я с этим не согласен private void Callback( Object state ) { // Long running operation _timer.Change( TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite ); }. Callbackможет быть вызван снова до завершения операции.
Алан Коромано
2
Я имел в виду, что тогда это Long running operationмогло занять гораздо больше времени TIME_INTERVAL_IN_MILLISECONDS. Что тогда будет?
Алан Коромано
32
Обратный звонок больше не будет вызываться, в этом суть. Вот почему мы передаем Timeout.Infinite в качестве второго параметра. Это в основном означает, что больше не ставьте галочку на таймере. Затем установите флажок после того, как мы завершим операцию.
Иван Златанов
Новичок в потоках здесь - как вы думаете, можно ли это сделать с помощью a ThreadPool, если вы передадите таймер? Я думаю о сценарии, в котором новый поток создается для выполнения задания с определенным интервалом, а затем передается в пул потоков по завершении.
jedd.ahyoung
2
System.Threading.Timer - это таймер пула потоков, который выполняет обратные вызовы в пуле потоков, а не в выделенном потоке. После того, как таймер завершает процедуру обратного вызова, поток, выполнивший обратный вызов, возвращается в пул.
Иван Златанов
14

Необязательно останавливать таймер, см. Хорошее решение из этого сообщения :

«Вы можете позволить таймеру продолжить запуск метода обратного вызова, но обернуть свой нереентерабельный код в Monitor.TryEnter / Exit. В этом случае нет необходимости останавливать / перезапускать таймер; перекрывающиеся вызовы не будут получать блокировку и немедленно возвращаться».

private void CreatorLoop(object state) 
 {
   if (Monitor.TryEnter(lockObject))
   {
     try
     {
       // Work here
     }
     finally
     {
       Monitor.Exit(lockObject);
     }
   }
 }
Иван Леоненко
источник
это не в моем случае. Мне нужно точно остановить таймер.
Алан Коромано,
Вы пытаетесь запретить ввод обратного вызова более одного раза? Если нет, то чего вы пытаетесь достичь?
Иван Леоненко
1. Запретить входить в обратный вызов более одного раза. 2. Предотвратить выполнение слишком большого количества раз.
Алан Коромано,
Это именно то, что он делает. # 2 не требует больших накладных расходов, если он возвращается сразу после оператора if, если объект заблокирован, особенно если у вас такой большой интервал.
Иван Леоненко
1
Это не гарантирует, что код будет вызван не менее чем через <интервал> после последнего выполнения (новый тик таймера может быть запущен через микросекунду после того, как предыдущий тик снял блокировку). Это зависит от того, строго ли это требование или нет (не совсем понятно из описания проблемы).
Marco Mp
9

Использует System.Threading.Timer обязательно?

Если нет, то System.Timers.Timerесть под рукой Start()и Stop()методы (и AutoResetсвойство можно установить в FALSE, так что Stop()не требуется , и вы просто звоните Start()после выполнения).

Марко Mp
источник
3
Да, но это могло быть реальным требованием, или просто так случилось, что таймер был выбран потому, что он наиболее часто используется. К сожалению, в .NET есть множество объектов таймера, перекрывающихся на 90%, но все же (иногда слегка) разных. Конечно, если это необходимо, это решение вообще не применяется.
Marco Mp
2
Согласно документации : класс Systems.Timer доступен только в .NET Framework. Он не включен в стандартную библиотеку .NET и недоступен на других платформах, таких как .NET Core или универсальная платформа Windows. На этих платформах, а также для обеспечения переносимости на все платформы .NET вместо этого следует использовать класс System.Threading.Timer.
NotAgain говорит: "Восстановить Монику"
3

Я бы просто сделал:

private static Timer timer;
 private static void Main()
 {
   timer = new Timer(_ => OnCallBack(), null, 1000 * 10,Timeout.Infinite); //in 10 seconds
   Console.ReadLine();
 }

  private static void OnCallBack()
  {
    timer.Dispose();
    Thread.Sleep(3000); //doing some long operation
    timer = new Timer(_ => OnCallBack(), null, 1000 * 10,Timeout.Infinite); //in 10 seconds
  }

И игнорируйте параметр периода, поскольку вы пытаетесь самостоятельно управлять периодичностью.


Ваш исходный код работает так же быстро , как это возможно, так как вы сохраняете указания 0для dueTimeпараметра. Откуда Timer.Change:

Если dueTime равен нулю (0), немедленно вызывается метод обратного вызова.

Damien_The_Unbeliever
источник
2
Нужно ли избавляться от таймера? Почему вы не используете Change()метод?
Алан Коромано,
21
Утилизировать таймер каждый раз совершенно не нужно и неправильно.
Иван Златанов
0
 var span = TimeSpan.FromMinutes(2);
 var t = Task.Factory.StartNew(async delegate / () =>
   {
        this.SomeAsync();
        await Task.Delay(span, source.Token);
  }, source.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

source.Cancel(true/or not);

// or use ThreadPool(whit defaul options thread) like this
Task.Start(()=>{...}), source.Token)

если вам нравится использовать какой-то цикл внутри ...

public async void RunForestRun(CancellationToken token)
{
  var t = await Task.Factory.StartNew(async delegate
   {
       while (true)
       {
           await Task.Delay(TimeSpan.FromSeconds(1), token)
                 .ContinueWith(task => { Console.WriteLine("End delay"); });
           this.PrintConsole(1);
        }
    }, token) // drop thread options to default values;
}

// And somewhere there
source.Cancel();
//or
token.ThrowIfCancellationRequested(); // try/ catch block requred.
Умка
источник