La ejecución en tiempo real generalmente se asocia a velocidad, pero es tan sólo una parte de toda la situación. En su concepto principal, la ejecución en tiempo real es todo acerca de predecibilidad: el saber que un sistema siempre va a ejecutarse dentro de un marco de tiempo requerido. Estos tiempos objetivo o metas no necesitan ser muy chicas en el tiempo (aunque generalmente lo son), y las consecuencias de no cumplir estas metas pueden no ser desastrosas (aunque a veces así resultan).
La clave para comprender si una aplicación es de tiempo real tiene que ver con sus requerimientos, y si los mismos incluyen restricciones temporales.
Los desarrolladores de la Especificación de Tiempo Real para Java (Real-Time Specification for Java, o RTSF), JSR 1, usan esta definición de ejecución en tiempo real:
El entorno de programación debe proveer las abstracciones necesarias para permitir a los desarrolladores manipular correctamente el comportamiento temporal de la lógica de la aplicación. No es necesariamente rápida, pequeña, o de uso exclusivo para el control industrial; todo es acerca de la predecibilidad de la ejecución de la lógica de la aplicación respecto al tiempo.
Las aplicaciones en tiempo real suelen ser particionadas en un grupo de tareas, algunos de los cuales tienen metas de tiempo. El objetivo es poder organizar a todas las tareas de tal manera que completen su trabajo antes de sus metas de tiempo.
Definiciones: tiempo real inflexible, tiempo real flexible
Se puede clasificar a los sistemas en tiempo real dependiendo de los requerimientos del sistema. Un sistema de tiempo real inflexible es aquel en el cual el sistema tiene que complir todas sus metas de tiempo sin excepción. Usualmente estos sistemas también tienen baja latencia, el tiempo entre que ocurre un evento disparador y se inicia o completa la respuesta a este evento, generalmente medido en microsegundos o milisegundos.
Muchos sistemas de tiempo real inflexible se los clasifica como sistemas de seguridad crítica. Estos sistemas se usan para proteger a humanos de resultar heridos o quedar en peligro. Los sistemas de seguridad crítica deben pasar por un testing exhaustivo y revisión de código línea-por-línea antes de ser certificados.
La tecnología Java para sistemas de seguridad críticos está siendo estudiada en JSR 302, y actualmente no forma parte de RTSJ.
Por otro lado, un sistema de tiempo real flexible es aquel que funcionará correctamente, de acuerdo a su especificación, aunque el sistema ocasionalmente no cumpla con las metas de tiempo. Por ejemplo, el sistema de telefonía celular es de tiempo real flexible: los clientes esperan que se completen todas las llamadas, pero no cumplir con metas de tiempo ocasionalmente puede causar un fallo aceptable, como pausas en el audio o demoras en la llamada. Los sistemas de tiempo real flexible especifican el porcentaje de metas que pueden incumplirse, y qué tan frecuente esto es aceptable.
Lo impredecible en las aplicaciones basadas en Java
Hay varios factores que llevan a la impredecibilidad de la ejecución de las aplicaciones en el tiempo, y por lo tanto pueden causar que una aplicación Java estándard no cumpla con las metas de tiempo. Las causas más comunes:
- Gestión del sistema operativo. En la tecnología Java, los threads son creados por la JVM pero, en última instancia, son gestionados por el sistema operativo. Para que una JVM pueda garantizar latencias temporales, es decir, la demora en el tiempo en reaccionar ante una acción, el sistema operativo tiene que brindar garantizas sobre la latencia de su gestión de threads.
- Inversión de prioridades. Un riesgo en una aplicación con threads es la inversión de prioridades. Si un thread de menor prioridad comparte un recurso con otro de mayor prioridad, y si el recurso está sincronizado por un monitor, el thread de menor prioridad podría tener bloqueado el recurso al momento que el thread de mayor prioridad lo necesita. En este caso, el thread de mayor prioridad no puede proceder hasta que el thread de menor prioridad termina su trabajo: y esto puede causar que el thread de mayor prioridad no cumpla con su meta de tiempo.
El efecto de la inversión de prioridad es que se "baja" la prioridad del thread de mayor prioridad a aquel de menor prioridad. - Carga de clases, inicialización y compilación. La especificación del lenguaje Java requiere que las clases sean inicializadas de manera perezosa: cuando una aplicación las utiliza por primera vez. Estas instanciaciones pueden ejecutar código de usuario, creando así latencias variables la primera vez que se usa la clase. Además, la especificación permite que las clases se carguen de manera perezosa también. Como la carga de clases pueden requerir accesos a disco o a través de una red para encontrar la definición de la clase, referenciar a una clase sin referencia previa puede ocasionar una demora inesperada (y potencialmente enorme). Por último, la JVM puede decidir cuando, si lo hace, traducir el código bytecode de la clase a código nativo. Usualmente se compila un método sólo cuando se ejecuta tan frecuentemente que justifica el costo de la compilación.
- Garbage collector. La principal fuente de impredeciblidad en Java es el Garbage Collector (GC). Los algoritmos de GC que usan las JVM estándard involucran, de una manera u otra, detener-el-mundo-y-limpiar: se suspenden todos los threads para que el GC puede ejecutarse sin interferencias. Las aplicaciones con requerimientos estrictos de tiempo no pueden tolerar estas pausas causadas por el GC. A pesar de todo el trabajo que se viene realizando para reducir estas pausas, incluso un GC de baja-pausa no es suficiente para garantizar la predecibilidad de la ejecución.
- La aplicación. Otra importante fuente de impredecibilidad es la misma aplicación, incluyendo a todas las librerias que utiliza. La mayoría de las aplicaciones consisten de diversas actividades que compiten de manera igual por los recursos del CPU. Las aplicaciones Java en general no utilizan threads con prioridad, en particular porque la JVM ofrece muy pocas garantías acerca de estas prioridades. Así, completar una tarea puede tardar más de lo esperado, lo que provoca que otras tareas tengan que esperar para recursos del CPU.
- Otras actividades en el sistema. Pueden existir otras actividades de alta prioridad en el tiema, como ser interrupciones de hardware, otras aplicaciones de tiempo real, y demás. Estas otras actividades pueden interferir con la aplicación, y así afectar el determinismo.
Java en Tiempo Real (RTSJ)
La Especificación de Tiempo Real para Java (RTSJ), o JSR 1, especifica cómo un sistema Java debería comportarse en un contexto de tiempo real. La especificación fue desarrollada durante varios años por expertos de Java y de aplicaciones en tiempo real.
La RTSJ está diseñada para extender la familia Java (toda la plataforma Java, Java SE, Java EE, Java Micro Edition y demás), y tiene el requerimiento de que cualquier implementación debe pasar el Test de Compatibilidad JSR 1 (TCK) y el TCK propio de la plataforma en la cual está basada. Es decir RTSJ extiende naturalmente cualquiera de las plataformas Java existentes.
RTSJ introduce varias caraterísticas nuevas para soportar operaciones en tiempo real. Estas características incluyen nuevos tipos de thread, nuevos modelos de gestión de memoria, y otros frameworks también nuevos.
Tareas y metas de tiempo
RTSJ modela la parte de tiempo real de una aplicación como un grupo de tareas, cada una de las cuales tiene una meta de tiempo opcional. Esta meta de la tarea es cuando la tarea debe ser completada. Las tareas de tiempo real se dividen en varias categorías, basadas en cómo el desarrollador puede predecir su frecuencia y ejecución:
- Periódicas: tareas que se ejecutan en una agenda fija, como ser leer un sensor cada 5 milisegundos.
- Esporádicas: tareas que no se ejecutan en una agenda fija, pero que tienen una frecuencia máxima.
- Aperiódicas: tareas cuya frecuencia y ejecución no pueden predecirse.
RTSJ utiliza la información de los tipos de tarea en distintas maneras para asegurar que las tareas críticas no incumplan sus metas. Primero, RTSJ permite asociarle a cada tarea un Handler de Meta Incumplida. Si una tarea no se completa antes de su meta de tiempo, se invoca al handler asociado.
La información de metas incumplidas puede ser usada para tomar medidas correctivas o reportar información de performance o compartiempo. En comparación con aplicaciones comunes (no de tiempo real), las fallas pueden no resultar aparentes sino por efectos secundarios (como ser timeouts), en cuyo caso puede ser demasiado tarde para arreglar la situación.
Un handler se podría configurar así:
ReleaseParameters.setDeadlineMissHandler(AsyncEventHandler handler);
Prioridades de threads
En un ambiente de tiempo real, las prioridades de los threads son de extrema importancia. Ningún sistema puede garantizar que todas las tareas vayan a completarse en tiempo. Sin embargo, un sistema de tiempo real puede garantizar que si algunas de las tareas van a incumplir sus metas de tiempo, entonces las tareas de menor prioridad serán las primeras en ser sacrificadas.
RTSJ define al menos 28 niveles de prioridad y requiere un cumplimiento estricto de las mismas. Sin embargo, como ya se mencionó, RTSJ depende de un sistema operativo en tiempo real para soportar múltiples prioridades.
El problema de la inversión de prioridades puede afectar negativamente a las prioridades. De esta manera, RTJS requeire la herencia de prioridades en su gestión. La herencia de prioridad evita la inversión de prioridades, aumentando la prioridad del thread que está bloqueando un recurso para igual la prioridad del thread de mayor prioridad que está esperando el recurso.
Esto evita que un thread de prioridad alta se queda esperando indefinidamente a un thread de menor prioridad que tiene bloqueado el recurso, pero que no puede liberar porque no obtiene los ciclos de CPU necesarios para finalizar su tarea.
Además, RTJS está diseñado para permitir que actividades de tiempo real y de no tiempo real coexistan en la misma aplicación Java. El grado de garantías temporales brindadas a una actividad dependen del tipo de thread en la cual se ejecuta la actividad: java.lang.Thread o javax.realtime.RealtimeThread.
- Los threads comunes java.lang.Thread (JLT) son soportados para actividades no de tiempo real. Estos threads JTL pueden usar 10 niveles de prioridad, pero no se brindan garantías de su ejecución en el tiempo.
- Los threads javax.realtime.RealtimeThread (RTT) toman ventaja de la gestión de threads con prioridad de RTJS, y se administración de una forma ejecutar-al-bloque, en lugar de por porciones de tiempo. Es decir, el administrador de threads sólo va a realizar un thread switch cuando un RTT de mayor prioridad esté listo para ejecutarse.
Extensiones para la gestión de memoria
Uno de los problemas de la gestión automática de memoria de las máquinas virtuales estándard es que una actividad puede "pagar" por el costo de la gestión de memoria de otra actividad. Considérese una aplicación con dos threads: un thread de alta prioridad (H) que realiza un muy poco uso de memoria, y un thread de baja prioridad (L) que utiliza mucha memoria.
Si H se ejecuta cuando L ya consumió casi toda la memoria disponible en el heap, el garbage collector puede activarse y consumir tiempo, cuando H sólo iba a asignar un objeto pequeño. Así, H está pagando (en la forma de una demora enorme) por el uso de memoria de L.
RTJS provee una subclase de RTT llamada NoHeapRealtimeThread (NHRT). Las instancias de esta subclase están protegidas de interferencias del GC. NHRT está pensanda para actividades de tiempo real inflexible.
Para maximizar la predecibilidad, los NHRT no pueden usar el heap gestionado por el garbage collector, no referenciar a este heap. Si lo hicieran, el thread podría sufrir pausas por el GC, lo que podría provocar incumplimientos en la meta de tiempo. En cambio, NHRT utilizan un memoria acotada y memoria inmortal para asignar memoria de una manera más predecible.
Áreas de memoria
RTSJ brinda varias formas de reservar memoria para objetos, dependiendo de la naturaleza de la tarea realizando la reserva. Los objetos pueden asignarse a un área de memoria específica, y las diferentes áreas tienen diferentes características de GC y límites de reserva.
- Heap estándard. Como cualquier máquina virtual, RTJS mantiene un heap con garbage collector, el cual puede ser usado por los threads estándard o de tiempo real. Los NHRT no pueden usar el heap estándard para poder garantizar predecibilidad.
- Memoria inmortal. La memoria inmortal es un área de memoria que no tiene un gargabe collector. Una vez que el objeto se asigna a la memoria inmortal, la memoria usada por este objeto nunca va a ser liberada (en principio). El uso principal de la memoria inmortal es que las actividades pueden evitar reservas de memoria dinámicas, reservando estáticamente la memoria que necesitan antes de la ejecución, y gestionándola ellas mismas.
Gestionar la memoria inmortal requiere mucho más cuidado que administrar la memoria del heap estándard. Para los que están familizarizados con C / C++, este mecanismo es similar al malloc() y free(). - Memoria acotada. RTSJ brinda un tercer mecanismo de reserva llamada memoria acotada, que sólo está disponible para threads de tiempo real (RTT y NHRT). Estas áreas de memoria están pensadas para objetos con un tiempo de vida conocido, como ser objetos temporales creados durante el procesamiento de una tarea. Al igual que la memoria inmortal, la memoria acotada no tiene un garbage collector, pero la diferencia es que el área completa de memori apuede reclamarse al finalizar su uso, como ser al fin de la tarea.
Estas áreas de memoria tienen un límite máximo asignado al crearse, de forma de poder controlar su uso y crecimiento.
¿Por qué memoria acotada?
En los programas java es comun reservar memoria para objetos temporales. Por ejemplo, al concatenar dos Strings se genera un StringBuffer que luego es descartado ni bien se transforma a un String.
La memoria acotdada permite a una tarea decir "voy a suar esta cantidad de memoria temporal durante el curso de mi tarea, y cuando la tarea esté terminada, reclamala toda".
Como la memoria acotada se reserva al comienzo de la tarea, la reserva no fallará. Dado que al finalizar la tarea se libera el área de memoria completa, la liberación es rápida y predecible. Por lo tanto, mientras las aplicaciones pueden estimar cuánta memoria van a necesitar, la memoria acotada permite eliminar la impredecibilidad de la reserva de memoria para objetos.
Una desventaja de este tipo de memoria es que, como todos sus objetos desaparecen luego de terminada la tarea, no se pueden guardar referencias a estos objetos en la memoria inmortal o el heap estandard.
En un procedimiento similar al de chequeos por null pointers y límites de los arrays, la JVM realiza controles dinámicos para garantizar que un objeto nunca es referenciado por un objeto con un tiempo de vida más largo.