c# - La devolución de llamada del temporizador se produce cada 24 horas: ¿se maneja correctamente el horario de verano?
service timer (3)
Si alguien obtuvo una respuesta sin la necesidad de un bucle de mensaje
La clase SystemEvents ya proporciona su propia ventana oculta y loop de despachador si no proporciona uno usted mismo. Puede parecer mágico que sepa que tiene uno, pero cumple un contrato bien establecido en la programación de Windows. Utiliza Thread.GetApartmentState () en el hilo que usa para agregar el controlador de eventos. Devuelve STA, al igual que en cualquier aplicación GUI, luego confía en que su programa implemente el contrato STA y encienda un bucle de mensajes y SystemEvents no haga nada especial.
Si devuelve MTA, al igual que en la aplicación o el servicio en modo consola, supone que su programa no tiene un bucle de despachador y admite el enhebrado como promesas de MTA e inicia un nuevo hilo. Puede ver ese hilo en la ventana Debug + Windows + Threads del depurador, el nombre del hilo es ".NET SystemEvents". Ese hilo crea una ventana oculta y bombea un bucle, equivalente a Application.Run (). Puedes ver esa ventana también con Spy ++, su nombre es ".NET-BroadcastEventWindow.xxxx".
Es notable que este comportamiento es responsable de la falla de muchos programas de la GUI, generalmente en el evento UserPreferenceChanged en una aplicación GUI y, por lo general, cuando el usuario desbloquea la estación de trabajo. Esto sucede cuando el programa crea una pantalla de bienvenida en un hilo de trabajo que no es STA. SystemEvents asume que el programa necesita ayuda y crea ese hilo de ayuda. Que ahora activa los eventos en el hilo equivocado, uno que el programa usa para actualizar su UI. También falla si es un subproceso STA pero ese subproceso tiene permiso para salir, SystemEvents intenta desencadenar el evento en un subproceso que ya no está allí y vuelve a un subproceso TP si falla. Eso es todo muy, muy malo en una aplicación GUI, el punto muerto es un resultado común.
Pero, por supuesto, en tu caso, te gusta mucho la forma en que funciona la clase SystemEvents, realmente quieres ese hilo de ayuda para que no tengas que escribirlo tú mismo. Solo tenga en cuenta que el evento TimeChanged se activa en un hilo completamente arbitrario que no está relacionado con ningún hilo que haya iniciado su servicio, por lo que se requiere un enclavamiento adecuado. El mismo problema que tienes con tu propio btw.
Considera la solución simple. Solo necesita calcular el intervalo del temporizador a partir de la diferencia entre la hora del reloj de pared de mañana y la hora actual. En otras palabras, tiempo absoluto, no incremental. Que producirá 23 o 25 horas si abarca un cambio de horario de verano.
Solo pensé en la forma en que resolví un servicio que ejecuta una tarea cada 24 horas y cómo el DST podría dañarlo.
Para ejecutar la tarea a diario, utilicé System.Threading.Timer con un período de 24 horas, como este:
_timer = new System.Threading.Timer(TimerCallback, null,
requiredTime - DateTime.Now, new TimeSpan(24, 0, 0));
De repente, pensando en la corrección del horario de verano tuve tres pensamientos:
- El horario de verano es inútil y deberíamos deshacernos de él.
- ¿El temporizador maneja esto correctamente? Creo que no, simplemente espera 24 horas, no importa si el reloj ha cambiado. Solo espera su período especificado y luego vuelve a llamar a TimerCallback.
- El horario de verano es inútil y realmente deberíamos deshacernos de él.
Es mi segundo pensamiento correcto? En caso afirmativo, ¿qué puedo hacer para evitar este problema? La tarea no se debe ejecutar una hora más tarde o antes si se ha producido una corrección DST.
SystemEvents.TimeChanged
le dirá si el reloj ha sido cambiado por el usuario . No dispara de forma regular con cada tic del reloj. Por lo tanto, no es útil para programar eventos, excepto que puede recalcular los temporizadores cuando ocurra. (Creo que también se activará si el sistema se sincroniza con un servidor de tiempo, pero no estoy seguro de eso).
Si intenta calcular la diferencia en el tiempo de pared como sugirió Hans, tenga cuidado. No puedes usar DateTime
. Observar:
// With time zone set for US Pacific time, there should only be 23 hours
// between these two points
DateTime a = new DateTime(2013, 03, 10, 0, 0, 0, DateTimeKind.Local);
DateTime b = new DateTime(2013, 03, 11, 0, 0, 0, DateTimeKind.Local);
TimeSpan t = b - a;
Debug.WriteLine(t.TotalHours); // 24
Incluso con el tipo local especificado, no tiene en cuenta el horario de verano.
Si va a seguir este enfoque, debe usar los tipos DateTimeOffset
.
DateTimeOffset a = new DateTimeOffset(2013, 03, 10, 0, 0, 0, TimeSpan.FromHours(-8));
DateTimeOffset b = new DateTimeOffset(2013, 03, 11, 0, 0, 0, TimeSpan.FromHours(-7));
TimeSpan t = b - a;
Debug.WriteLine(t.TotalHours); // 23
Probablemente necesites reunir tus entradas con algo como esto:
DateTime today = DateTime.Today; // today at midnight
DateTime tomorrow = today.AddDays(1); // tomorrow at midnight
TimeZoneInfo tz = TimeZoneInfo.Local;
DateTimeOffset a = new DateTimeOffset(today, tz.GetUtcOffset(today));
DateTimeOffset b = new DateTimeOffset(tomorrow, tz.GetUtcOffset(tomorrow));
TimeSpan t = b - a;
Debug.WriteLine(t.TotalHours); // 23, 24, or 25 depending on DST
Sin embargo , probablemente no sea una gran idea configurar un solo temporizador para que funcione durante un período de tiempo tan largo. El usuario no solo puede cambiar el reloj o sincronizar el tiempo del sistema, sino que la aplicación o el sistema pueden apagarse o reiniciarse. Además, si tiene muchas de estas tareas, puede terminar consumiendo muchos recursos solo para quedarse inactivo.
Una idea sería mantener una lista con la próxima vez para disparar eventos. En su aplicación, debe activar un temporizador de sondeo de corta duración (por ejemplo, una vez por minuto) y comparar la hora actual con los valores de la lista para saber si realmente necesita hacer algo.
Otra idea sería una ligera variación de eso. En lugar de una encuesta corta, debería mantener su lista ordenada y establecer un tiempo para la demora hasta el siguiente evento, con un retraso máximo (quizás una hora). Nuevamente, cuando se dispara el temporizador, verá si hay algo que hacer, o si necesita establecer otro retraso del temporizador. Debe ejecutar esto cuando se inicie su aplicación, y cada vez que se programe un evento nuevo.
Con cualquiera de estos enfoques, debe programar usando un DateTimeOffset
o el UTC DateTime
equivalente. De lo contrario, corre el riesgo de disparar en el momento incorrecto para el horario de verano, o incluso de disparar dos veces durante la transición de retroceso del horario de verano.
Si todo eso suena demasiado complicado, entonces puede probar una solución preconstruida , como Quartz.net . En particular, lea esta sección de sus preguntas frecuentes .
En cuanto a su primer y tercer punto, estoy totalmente de acuerdo, pero nunca va a suceder. Incluso si lo hace, todavía tendremos que considerar todos los muchos años de historia que ocurrió. Si aún no has visto este video , deberías hacerlo.
Encontré mi solución, aunque probablemente no sea la mejor para un servicio de Windows.
Ahora uso SystemEvents.TimeChanged
para obtener un evento cuando se ha cambiado la hora del sistema (sorpresa), sin embargo, este evento requiere un bucle de mensaje como el de un Formulario.
La respuesta a esta pregunta me ayudó a implementar el bucle de mensajes en un servicio de Windows: https://.com/a/9807963/777985
Si alguien obtuvo una respuesta sin la necesidad de dicho ciclo de mensajes, lo aceptaré.