Acelerar el Timer en Windows

Las clases Timer y Xojo.Core.Timer proporcionan una resolución más que suficiente para la mayoría de los usos que podemos requerir. De hecho, bajo macOS no existe ningún tipo de limitación en este sentido utilizando las clases proporcionadas por el Framework de Xojo, puesto que obtendremos en todos los casos una resolución máxima de 1 ms cuando necesitamos la mayor velocidad de respuesta (de hecho, en la mayoría de los proyectos en los que he participado suele ser suficiente con una resolución de 5 ms). Pero la cosa cambia en Windows. En este caso, no importa que ajustemos la propiedad Period a un valor mínimo de 1 ms, en cualquier caso, la máxima resolución que obtendremos es la que nos proporciona por omisión el sistema operativo: 16 ms; insuficiente para casos donde la precisión (velocidad de disparo del evento) resulta crítica. ¿Existe alguna solución? Sigue leyendo y te cuento una técnica que podrías encontrar útil.

Empecemos por las bases. El periodo del Timer está determinado en primer término por el soporte del reloj interno del hardware y, especialmente, por el driver del sistema operativo. Actualmente, y bajo Windows, todos los ordenadores modernos son capaces de proporcionar una resolución superior a los 16 ms impuestos de serie por el sistema operativo.

De hecho, cuando utilizamos la función NtQueryTimerResolution de la librería NTDLL.dll, mediante la cual podemos obtener los valores mínimo, máximo y actual de temporización para el equipo, obtendremos unos valores similares a los siguientes (los obtenidos en mi equipo, ejecutando Windows bajo una máquina virtual VMWare Fusion):

  • Resolución Mínima: 15,62 ms
  • Resolución Máxima: 0,5 ms
  • Resolución Actual: 0,99 ms

Como puedes ver, el primer valor coincide con el funcionamiento habitual de los temporizadores bajo Windows, y también el mismo valor mínimo de disparo que puedes esperar cuando utilizamos los Timer de Xojo en las aplicaciones Windows. Ahora bien, el hardware nos indica que de hecho podríamos alcanzar una resolución máxima incluso de medio milisegundo.

Para realizar dicho ajuste, Windows nos ofrece varias opciones (todas ellas mediante el uso de Declares o bien definición de Métodos Externos (más cómodo y práctico).

La primera de estas opciones, y que sería la óptima, se encuentra en el conjunto de funciones reunidas bajo el uso de temporizadores multimedia o de alta resolución. Se trata del conjunto de funciones timeBeginPeriod, mediante la cual podemos ajustar el periodo mínimo de disparo a 1ms, por ejemplo, y especialmente las siguientes:

  • CreateTimerQueueTimer: Nos permite crear un temporizador con una función delegada (Callback); tanto de disparo único como de disparo regular con el intervalo indicado. Es decir, un comportamiento verdaderamente similar al encontrado en los Timer de Xojo.
  • ChangeTimerQueueTimer: Nos permite modificar el intervalo de disparo para un temporizador ya creado, por ejemplo utilizando la anterior función.

El problema cuando utilizamos este enfoque es que Windows creará estos temporizadores en un hilo de ejecución diferente al utilizado por nuestra aplicación Xojo. En mis pruebas, esto representa un problema cuando queremos utilizar tiempos de disparo bajos (los que nos interesan) debido precisamente a que actualmente Xojo no soporta hilos pre-emptivos. El resultado es que nuestras aplicaciones interrumpirían su funcionamiento de forma inesperada debido a que el Framework no tendría conocimiento de dicho hilo de ejecución… y, por tanto, no podría coordinarse correctamente con el trabajo realizado internamente por el propio framework cuando se está ejecutando la aplicación (principalmente, objetos que apuntan a Nil en un momento determinado).

Por otra parte, también deberíamos de tener en cuenta que este tipo de temporizadores son re-entrantes. Es decir, si establecemos periodos de disparo realmente bajos (1ms), la función definida en el Callback volverá a ejecutarse, independientemente de que hubiese terminado la ejecución de la anterior llamada como si no.

En mis pruebas encontré la forma de solucionarlo mediante la creación de temporizadores de disparo único. Así, sería la propia función definida como Callback la encargada de borrar el anterior temporizador (es necesario hacerlo para no desborar el límite de 500 temporizadores como máximo) y crear un nuevo temporizador con la misma resolución… y también de disparo único.

De todas formas, este acercamiento no sirve para eliminar el principal obstáculo: en cualquier momento, de forma aleatoria, la aplicación saldrá inesperadamente debido a la imposibilidad de coordinar el hilo de ejecución con los procesos realizados internamente por el Framework de Xojo.

Temporizadores en el mismo hilo

Una vez descartada la anterior posibilidad, la técnica que puedes utilizar está basada en otro tipo de temporizadores proporcionados por Windows: los WaitableTimer. Este tipo de temporizadores se crean y, lo más importante, se ejecutan en el mismo hilo de proceso en el que son creados. Por tanto, no colisionan con el funcionamiento interno del framework durante la ejecución de las aplicaciones que lo utilicen. Además, al igual que ocurre con los temporizadores multimedia (o de alta definición), también nos permitirán obtener una resolución mínima de 1 ms.

Para comenzar a crear y utilizar este tipo de temporizadores en nuestras aplicaciones Xojo, lo primero que tendremos que hacer será crear un Método Externo para definir la signatura del mismo, trasladado desde la librería de C (o C++) de Windows.

Por tanto, en un nuevo proyecto de Xojo, crea un Módulo y añade a este un nuevo Método Externo con los siguientes valores:

  • Method Name: CreateWaitableTimerW
  • Parameters: att as ptr, resetMode as Boolean, timerName as CString
  • Return Type: Uint32
  • Lib: Kernel32.dll
  • Soft: Activado

Como puedes ver, esta función nos devolverá un valor de tipo Uint32 que será distinto de cero en el caso de que se haya podido crear el Timer con éxito. De hecho, se trata de un Handler apuntando a la estructura del Temporizador. Por tanto, es más que recomendable que crees en tus proyectos una Propiedad de ámbito público de modo que puedas volver a acceder a este valor en la llamada a otras funciones necesarias para borrar (cancelar) el Timer y también para liberar el propio Handler, recuperando así la memoria utilizada por la estructura a la cual apunta.

Desde el punto de vista de código Xojo, el modo en el que podremos utilizar esta función sería de la siguiente forma:

Dim name As CString = "name"
handler = CreateWaitableTimerW(Nil, False,  name)

Aquí, la variable handler es una propiedad definida en la clase que sirve como wrapper de la funcionalidad (échale un vistazo al proyecto de ejemplo).

Una vez obtenido el Handler, ya podremos definir los parámetros de su funcionamiento empleando para ello la función SetWaitableTimer. Al igual que en el anterior caso, crearemos un Método Externo con la siguiente signatura:

  • Method Name: SetWaitableTimer
  • Parameters: hTimer as uint32, byref lpDueTime as LARGEINTEGER, period as uint32, CompletionRoutine as ptr, parameter as uint32, resume as Boolean
  • Return Type: Boolean
  • Lib: Kernel32.dll
  • Soft: Activado

Como puedes observar en la signatura del método, estamos utilizando un dato no disponible en el framework de Xojo: LARGE_INTEGER. Se trata de la estructura de datos que utiliza la llamada a la función (de hecho, espera un puntero a dicha estructura, de ahí que definamos el parámetro en Xojo utilizando la palabra clave ByRef. Este es el modo de pasar el puntero a nuestra estructura.

Por tanto, hemos de añadir a nuestro proyecto la definición de dicha estructura con los siguientes valores:

  • Structure Name: LARGE_INTEGER

Y añadiendo el siguiente valor en el editor correspondiente a Declaration para la estructura:

  • quadPart As Int64.

Ajustar los valores del temporizador

Una vez hecho esto, ya podremos escribir el código correspondiente en Xojo para llamar a nuestra función:

pLarge.quadPart = -5
b = SetWaitableTimer( handler, pLarge, TimerPeriod, AddressOf callback, 0, False)

Los argumentos importantes pasados a la función en este caso son los siguientes:

  • Handler: será el valor (distinto de cero) que hayamos obtenido en la llamada a la anterior función.
  • pLarge: es una propiedad añadida al proyecto y cuyo tipo de dato se corresponde con la estructura definida. Como ves, hemos asignado previamente el valor entero negativo -5. Es muy importante indicar aquí un valor, o punto de partida, correspondiente a un punto de tiempo en el pasado (en este caso, cinco milisegundos).
  • TimerPeriod: Aquí pasaremos el intervalo de tiempo con el que deseamos ejecutar el temporizador (por ejemplo, 1 ms.)
  • AddressOf callback: Aquí estamos pasando la dirección del método que deseamos ejecutar cada vez que se dispare el temporizador. Es muy imporante que añadas el método a tu proyecto como Shared Method (método compartido). En nuestro ejemplo, el método se llama callback.

La llamada al método nos devolverá un valor Booleano que será True en el caso de que se haya ejecutado con éxito, o bien False en el caso contrario.

Señalizar el disparo del Timer

Sin embargo, este tipo de temporiadores precisan de ser señalizado para su ejecución. Esto significa que hemos de provocarlo utilizando otra función de la librería Kernel32.DLL: se trata de SleepEx.

Como en los anteriores casos, añadiremos un nuevo Método Externo utilizando los siguientes valores en su signatura:

  • Method Name: SleepEx
  • Parameters: miliseconds as uint32, bAlertable as Boolean
  • Return Type: Uint32
  • Lib: Kernel32.DLL
  • Soft: True

Una vez definido, deberemos de crear un bucle en nuesrtro código Xojo para que el Timer sea señalizado constantemente (es decir, se ejecute más de una vez). Si esta no es tu necesidad, entocnes obvia el bucle:

Do
result = SleepEx( INFINITE, True)
app.DoEvents
Loop Until CancelTimer = True

Aquí se está utilizando la propiedad CancelTimer de tipo Booleano para proporcionarnos una vía de interrumpir la ejecución del bucle en algún momento durante la ejecución del programa (es decir, interrumpir el funcionamiento del Timer creado).

Otro parámetro importante es el utilizado en la llamada a la función SleepEx: INFINITE. Se trata de una Constante añadida al proyecto con el valor hexadecimal &hFFFFFFFF.

Limpieza del Timer

A diferencia de lo que ocurre con los Timer multimedia de Windows, en este caso no es posible modificar el valor de disparo para un timer ya creado; de modo que el único modo de hacerlo consiste en borrar el timer en uso y volver a crear uno nuevo; algo que también tendrás que hacer al salir de la aplicación o cuando, simplemente, quieras de ejecutar a intervalos regulares la función definida como Callback.

Para ello debemos de echar mano de otra función: CancelWaitableTimer. Al igual que en anteriores casos, la añadiremos como Método Externo en Xojo utilizando la siguiente signatura:

  • Method Name: CancelWaitableTimer
  • Parameters: thandler as uint32
  • Return Type: Boolean
  • Lib: Kernel32.DLL
  • Soft: Activado

Adicionalmente, y como señalaba anteriormente, también será necesario como parte del proceso de limpieza liberar el propio Handler utilizado. Para ello, hemos de añadir un Método Externo adicional con la siguiente signatura:

  • Method Name: CloseHandle
  • Parameters: tHandler as Uint32
  • Return Type: Boolean
  • Lib: Kernel32.dll
  • Soft: Activado

Ahora, en Xojo ya podrás utilizar el siguiente código como parte del proceso de limpieza del Timer:

If handler <> 0 Then
Dim b As Boolean
b = CancelWaitableTimer( handler )
b = CloseHandle( handler )
End If

Conclusiones

Como puedes observar, el principal inconveniente de esta técnica es que se interrumpe el funcionamiento habitua de la aplicación una vez que se entra en el método encargado de crear y disparar ininterrumpidamente el temporizado. Es decir, el método no devuelve el control (no retorna) hasta que no salgamos del bucle infinito que, por otra parte, necesitamos para continuar señalizando el temporizador a los intervalos regulares indicados. El único modo en el que nuestra aplicación continuará realizando otras acciones será gracias a la línea App.DoEvents, de modo que forzamos la actualización de un nuevo ciclo de Eventos: los elementos de interfaz gráfica seguirán reaccionando a las interacciones del usuario y, por tanto, ejecutará el código asociado.

Lectura adicional:

Proyecto de ejemplo

Desde el proyecto de ejemplo, que puedes descargar desde este enlace, se muestra lo siguiente:

  • Funcionamiento de la técnica expuesta (temporizadores con una resolución máxima de 1ms a intervalos regulares).
  • Como modificar el periodo de ejecución mediante la destrucción y creación de un nuevo temporizador.
  • Modificar al vuelo el código ejecutado por el Callback definido para el temporizador
  • Valores de resolución actual, máximo y mínimo correspondientes al reloj interno del equipo sobre el cual se ejecuta la aplicación.

4 comentarios en “Acelerar el Timer en Windows

  1. Fernando Pinto Molina

    Gracias por el articulo,me fue de gran ayuda,me gustaria conocer las experiencias de los timer en la Web

    1. Javier Rodriguez

      Gracias por tu comentario, Fernando.

      Los Timer Web se manejan desde la propia página del cliente… creo que hay poco que rascar aquí más allá de las capacidades proporcionadas por el propio Framework. En estos caso, no obstante, la resolución que proporcionan debería de ser más que suficiente para el tipo de tareas que deberían de llevar a cabo.

  2. Michael Jauregui

    Pregunta, ejecutar código en intervalos demasiado bajos (0.5~1ms) no seria peligroso para la estabilidad del sistema? Al menos usando lenguajes de alto nivel, recuerdo haber leido que el .NET Framework se vuelve inestable usando intervalos inferiores a 40 en la clase Timer de WinForms.

    1. Javier Rodriguez

      Windows proporciona las DLL porque hay aplicaciones que requieren de este tipo de precisión (especialmente utilizando los MultimediaTimer). Serían impensables de otro modo. Establece, por omisión, un pool máximo de 500 timers simultáneos (lo que es una buena cifra, en mi opinión).

      En todas mis pruebas, y sobre máquina virtual (que se supone más lenta y limitada en recursos), no he tenido pruebas de estabilidad… eso sí, con ejemplos / apps sencillas; pero hay aplicaciones comerciales que hacen uso de estos recursos, no lo dudes.

      Imagino que la inestabilidad vendría si existen varios (muchos) procesos en simultáneo que sí hiciesen uso de este tipo de resolución en el Timer. No he llegado tan lejos, ahí no te puedo decir.

      Quizá el Timer de WinForms sea más sensible a estas cuestiones, lo desconozco. Ya puedes ver como funciona en Xojo (y existe un plug-in comercial para Xojo que incluso lo hace mucho mejor, manejando sus propios hilos preemptivos y funciones con re entrada).

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *