System.Threading.Timer en C # parece no estar funcionando. Corre muy rápido cada 3 segundos.

112

Tengo un objeto de temporizador. Quiero que se ejecute cada minuto. Específicamente, debería ejecutar un OnCallBackmétodo y permanecer inactivo mientras OnCallBackse ejecuta un método. Una vez que finaliza un OnCallBackmétodo, (a OnCallBack) reinicia un temporizador.

Esto es lo que tengo ahora mismo:

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
}

Sin embargo, parece que no funciona. Corre muy rápido cada 3 segundos. Incluso cuando se eleva un punto (1000 * 10). Parece que se hace de la vista gorda1000 * 10

¿Qué hice mal?

Alan Coromano
fuente
12
De Timer.Change: "Si dueTime es cero (0), el método de devolución de llamada se invoca inmediatamente". Parece que es cero para mí.
Damien_The_Unbeliever
2
Sí, pero ¿y qué? también hay un período.
Alan Coromano
10
Entonces, ¿qué pasa si también hay un período? La oración citada no hace afirmaciones sobre el valor del período. Solo dice "si este valor es cero, invocaré la devolución de llamada inmediatamente".
Damien_The_Unbeliever
3
Curiosamente, si establece dueTime y period en 0, el temporizador se ejecutará cada segundo y comenzará de inmediato.
Kelvin

Respuestas:

230

Este no es el uso correcto de System.Threading.Timer. Cuando crea una instancia del temporizador, casi siempre debe hacer lo siguiente:

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

Esto le indicará al temporizador que marque solo una vez cuando haya transcurrido el intervalo. Luego, en su función de devolución de llamada, cambia el temporizador una vez que se ha completado el trabajo, no antes. Ejemplo:

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

Por tanto, no hay necesidad de mecanismos de bloqueo porque no hay concurrencia. El temporizador disparará la siguiente devolución de llamada después de que haya transcurrido el siguiente intervalo + el tiempo de la operación de larga duración.

Si necesita ejecutar su temporizador exactamente en N milisegundos, le sugiero que mida el tiempo de la operación de larga duración usando Cronómetro y luego llame al método Cambiar de manera apropiada:

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

Recomiendo encarecidamente a cualquiera que esté usando .NET y esté usando CLR que no haya leído el libro de Jeffrey Richter: CLR a través de C # , que lo lea lo antes posible. Los temporizadores y los grupos de hilos se explican con gran detalle allí.

Ivan Zlatanov
fuente
6
No estoy de acuerdo con eso private void Callback( Object state ) { // Long running operation _timer.Change( TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite ); }. Callbackse puede volver a llamar antes de que se complete una operación.
Alan Coromano
2
Lo que quise decir es que Long running operationpodría llevar mucho más tiempo entonces TIME_INTERVAL_IN_MILLISECONDS. ¿Qué pasaría entonces?
Alan Coromano
31
La devolución de llamada no se volverá a llamar, este es el punto. Por eso pasamos Timeout.Infinite como segundo parámetro. Esto básicamente significa que no vuelva a marcar el temporizador. Luego, reprograme para marcar una vez que hayamos completado la operación.
Ivan Zlatanov
Novato en el enhebrado aquí: ¿crees que esto se puede hacer con a ThreadPool, si pasas el temporizador? Estoy pensando en un escenario en el que se genera un nuevo hilo para hacer un trabajo en un cierto intervalo, y luego se relega al grupo de hilos cuando se completa.
jedd.ahyoung
2
System.Threading.Timer es un temporizador de grupo de subprocesos, que está ejecutando sus devoluciones de llamada en el grupo de subprocesos, no un subproceso dedicado. Una vez que el temporizador completa la rutina de devolución de llamada, el hilo que ejecutó la devolución de llamada vuelve al grupo.
Ivan Zlatanov
14

No es necesario detener el temporizador, vea una buena solución en esta publicación :

"Puede dejar que el temporizador continúe activando el método de devolución de llamada, pero envuelva su código no reentrante en un Monitor. TryEnter / Exit. No es necesario detener / reiniciar el temporizador en ese caso; las llamadas superpuestas no adquirirán el bloqueo y regresarán inmediatamente".

private void CreatorLoop(object state) 
 {
   if (Monitor.TryEnter(lockObject))
   {
     try
     {
       // Work here
     }
     finally
     {
       Monitor.Exit(lockObject);
     }
   }
 }
Ivan Leonenko
fuente
no es para mi caso. Necesito detener el temporizador exactamente.
Alan Coromano
¿Está tratando de evitar ingresar la devolución de llamada más de una vez? Si no, ¿qué estás tratando de lograr?
Ivan Leonenko
1. Evite ingresar para devolver la llamada más de una vez. 2. Evite ejecutar demasiadas veces.
Alan Coromano
Esto es exactamente lo que hace. # 2 no es mucho más general siempre y cuando regrese justo después de la instrucción if si el objeto está bloqueado, especialmente si tiene un intervalo tan grande.
Ivan Leonenko
1
Esto no garantiza que el código se llame no menos de <intervalo> después de la última ejecución (un nuevo tic del temporizador podría dispararse un microsegundo después de que un tic anterior liberara el bloqueo). Depende de si este es un requisito estricto o no (no está del todo claro en la descripción del problema).
Marco Mp
9

¿Es System.Threading.Timerobligatorio el uso ?

Si no es así, System.Timers.Timertiene métodos prácticos Start()y Stop()(y una AutoResetpropiedad que puede establecer en falso, para que Stop()no sea necesario y simplemente llame Start()después de ejecutar).

Marco Mp
fuente
3
Sí, pero podría ser un requisito real, o simplemente sucedió que se eligió el temporizador porque es el más utilizado. Lamentablemente .NET tiene toneladas de objetos de temporizador, superpuestos en un 90% pero aún siendo (a veces sutilmente) diferentes. Por supuesto, si es un requisito, esta solución no se aplica en absoluto.
Marco Mp
2
Según la documentación : La clase Systems.Timer solo está disponible en .NET Framework. No se incluye en la biblioteca estándar de .NET y no está disponible en otras plataformas, como .NET Core o la plataforma universal de Windows. En estas plataformas, así como para la portabilidad en todas las plataformas .NET, debe usar la clase System.Threading.Timer en su lugar.
NotAgain dice Reincorporar a Monica
3

Yo solo haría:

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 el parámetro de período, ya que está intentando controlar la periodicidad usted mismo.


Su código original se está ejecutando lo más rápido posible, ya que sigue especificando 0el dueTimeparámetro. De Timer.Change:

Si dueTime es cero (0), el método de devolución de llamada se invoca inmediatamente.

Damien_The_Unbeliever
fuente
2
¿Es necesario desechar el temporizador? ¿Por qué no usas el Change()método?
Alan Coromano
21
Desechar el temporizador cada vez es absolutamente innecesario y incorrecto.
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)

si quieres usar un hilo de bucle 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
fuente