System.Threading.Timer em C # parece não estar funcionando. Corre muito rápido a cada 3 segundos

112

Eu tenho um objeto temporizador. Eu quero que seja executado a cada minuto. Especificamente, ele deve executar um OnCallBackmétodo e ficar inativo enquanto um OnCallBackmétodo está sendo executado. Quando um OnCallBackmétodo termina, ele (a OnCallBack) reinicia um cronômetro.

Aqui está o que tenho agora:

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
}

No entanto, parece não estar funcionando. Corre muito rápido a cada 3 segundos. Mesmo quando se levanta um período (1000 * 10). Parece que fecha os olhos para1000 * 10

O que eu fiz errado?

Alan Coromano
fonte
12
De Timer.Change: "Se dueTime for zero (0), o método de retorno de chamada é invocado imediatamente.". Parece que é zero para mim.
Damien_The_Unbeliever,
2
Sim, mas e daí? há um período também.
Alan Coromano,
10
E se houver um período também? A frase citada não faz nenhuma reclamação sobre o valor do período. Diz apenas "se este valor for zero, vou invocar o retorno de chamada imediatamente".
Damien_The_Unbeliever,
3
Curiosamente, se você definir dueTime e period como 0, o cronômetro será executado a cada segundo e será iniciado imediatamente.
Kelvin de

Respostas:

230

Este não é o uso correto do System.Threading.Timer. Ao instanciar o Timer, você deve quase sempre fazer o seguinte:

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

Isso instruirá o cronômetro a marcar apenas uma vez quando o intervalo tiver decorrido. Em seguida, em sua função de retorno de chamada, você altera o cronômetro quando o trabalho é concluído, não antes. Exemplo:

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

Portanto, não há necessidade de mecanismos de bloqueio porque não há concorrência. O cronômetro irá disparar o próximo retorno de chamada após o próximo intervalo ter decorrido + o tempo da operação de longa duração.

Se você precisa executar seu cronômetro em exatamente N milissegundos, sugiro que você meça o tempo da operação de longa duração usando o cronômetro e chame o método Change apropriadamente:

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 );
}

Eu encorajo fortemente qualquer um que esteja usando .NET e esteja usando o CLR que não leu o livro de Jeffrey Richter - CLR via C # , para ler o mais rápido possível. Timers e pools de thread são explicados em detalhes aqui.

Ivan Zlatanov
fonte
6
Eu não concordo com isso private void Callback( Object state ) { // Long running operation _timer.Change( TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite ); }. Callbackpode ser chamado novamente antes que uma operação seja concluída.
Alan Coromano,
2
O que eu quis dizer é que Long running operationpode levar muito mais tempo TIME_INTERVAL_IN_MILLISECONDS. O que aconteceria então?
Alan Coromano,
31
O retorno de chamada não será chamado novamente, este é o ponto. É por isso que passamos Timeout.Infinite como um segundo parâmetro. Isso basicamente significa não marcar novamente para o cronômetro. Em seguida, reprograme para marcar depois de concluirmos a operação.
Ivan Zlatanov,
Novato em threading aqui - você acha que isso é possível fazer com um ThreadPool, se você passar o cronômetro? Estou pensando em um cenário onde um novo thread é gerado para fazer um trabalho em um determinado intervalo - e então relegado para o pool de threads quando concluído.
jedd.ahyoung
2
O System.Threading.Timer é um temporizador de pool de thread, que executa seus callbacks no pool de thread, não um thread dedicado. Depois que o cronômetro conclui a rotina de retorno de chamada, o encadeamento que o executou volta ao pool.
Ivan Zlatanov,
14

Não é necessário parar o cronômetro, veja uma boa solução neste post :

"Você poderia deixar o cronômetro continuar disparando o método de retorno de chamada, mas envolver seu código não reentrante em um Monitor.TenteEntrar / Sair. Não há necessidade de parar / reiniciar o cronômetro nesse caso; chamadas sobrepostas não obterão o bloqueio e retornarão imediatamente."

private void CreatorLoop(object state) 
 {
   if (Monitor.TryEnter(lockObject))
   {
     try
     {
       // Work here
     }
     finally
     {
       Monitor.Exit(lockObject);
     }
   }
 }
Ivan Leonenko
fonte
não é para o meu caso. Preciso parar o cronômetro exatamente.
Alan Coromano,
Você está tentando evitar a entrada de retorno de chamada mais de uma vez? Se não, o que você está tentando alcançar?
Ivan Leonenko,
1. Evite entrar no retorno de chamada mais de uma vez. 2. Evite executar muitas vezes.
Alan Coromano,
É exatamente isso que faz. # 2 não é muito overhead, contanto que retorne logo após a instrução if se o objeto estiver bloqueado, especialmente se você tiver um intervalo tão grande.
Ivan Leonenko,
1
Isso não garante que o código seja chamado pelo menos <interval> após a última execução (um novo tique do cronômetro pode ser disparado um microssegundo depois que um tique anterior liberou o bloqueio). Depende se este é um requisito estrito ou não (não totalmente claro na descrição do problema).
Marco Mp
9

Está usando System.Threading.Timerobrigatório?

Se não, System.Timers.Timertem métodos Start()e úteis Stop()(e uma AutoResetpropriedade que você pode definir como falsa, de forma que o Stop()não seja necessário e você simplesmente chama Start()após a execução).

Marco Mp
fonte
3
Sim, mas pode ser um requisito real, ou simplesmente aconteceu que o temporizador foi escolhido por ser o mais utilizado. Infelizmente, o .NET tem toneladas de objetos de cronômetro, sobrepostos em 90%, mas ainda sendo (às vezes sutilmente) diferentes. Claro, se for um requisito, esta solução não se aplica de forma alguma.
Marco Mp
2
De acordo com a documentação : A classe Systems.Timer está disponível apenas no .NET Framework. Ele não está incluído na Biblioteca .NET Standard e não está disponível em outras plataformas, como .NET Core ou Plataforma Universal do Windows. Nessas plataformas, bem como para portabilidade em todas as plataformas .NET, você deve usar a classe System.Threading.Timer.
NotAgain diz Restabelecer Monica
3

Eu apenas faria:

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
  }

E ignore o parâmetro de período, já que você está tentando controlar o período por conta própria.


Seu código original está sendo executado o mais rápido possível, já que você continua especificando 0o dueTimeparâmetro. De Timer.Change:

Se dueTime for zero (0), o método de retorno de chamada será invocado imediatamente.

Damien_The_Unbeliever
fonte
2
É necessário descartar o cronômetro? Por que você não usa o Change()método?
Alan Coromano,
21
Descartar o cronômetro todas as vezes é absolutamente desnecessário e errado.
Ivan Zlatanov,
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)

se você quiser, use algum fio de loop dentro ...

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.
Umka
fonte