La Arquitectura de Componentes Autónomos

translated by Juan Martínez Romo, juaner@gsyc.es

13 de mayo de 2006

Este documento introduce la arquitectura de componentes autónomos (ACA). Comenzamos con nuestra motivación técnica para proponer otra arquitectura basada en componentes.  A continuación veremos los detalles.  Específicamente, introducimos la noción de componentes, puertos, y contratos, discutimos cómo se identifican los componentes y los puertos, cómo los componentes importan/exportan información en tiempo de ejecución, y una implementación de ACA para simulaciones, llamado J-Sim.  En el documento titulado “Modelo Abstracto de Red y J-Sim,” nos centraremos en la simulación de red, y presentaremos un modelo generalizado de red de intercambio de paquetes, la plataforma de simulación entre redes(INET).

Contenido


1 Motivación Técnica

En el diseño moderno de los circuitos digitales, un sistema de hardware está ensamblado a partir de chips de circuitos integrados (IC) en una placa con un circuito impreso. Un chip con un IC es una caja negra en la que la especificación de su función y los patrones de la señal de entrada/salida se encuentran completamente especificados en su manual . Cambios en las señales de entrada accionan cierta función del IC, y cambia, después de un cierto retardo , sus salidas según la especificación del chip. El hecho de que un chip con un IC esté interconectado con otros chips/módulos/sistemas solamente a través de sus patillas (y se blinda además del resto del mundo) permite que los chips con IC sean diseñados, implementados y probados, independientemente de todo los demás.  Creemos que éste es uno de los factores dominantes porqué la industria del IC es tan exitosa.  

Nuestra propósito de una arquitectura de componentes autónomos es una tentativa directa imitando el diseño del IC y el modelo de fabricación en términos de cómo se especifican, se diseñan, y están montados los componentes.  Uno puede discutir que el paradigma de programación orientada a objetos (OO) fuera propuesto exactamente para el mismo propósito.  Sí, pero esto no es bastante para alcanzar el objetivo. Echemos una ojeada cómo la función que calcula un x es implementada en el paradigma de programación OO.   En el paradigma de programación procesal, el código puede parecerse a:

double exp_a(double a, double x)
{
double tmp = x * ln(a);
double ans = exp(tmp);
return ans;
}

En el paradigma de programación OO, uno puede empaquetar la función en una clase ExtendedMath (que alternadamente llame a ln () y exp () en una clase BasicMath):

class ExtendedMath
{
BasicMath bMath;

double exp_a(double a, double x)
{
double tmp = x * bMath.ln(a);
double ans = bMath.exp(tmp);
return ans;
}

.....

}

 

Figura 1: Relación de las dos clases, ExtendedMath y BasicMath.

 

Como se muestra en la imagen 1, el modelo de programación OO posee una cierta semejanza al diseño del IC y al modelo de fabricación: un sistema de software se compone de componentes que interactúan el uno con el otro. Como los datos se dan a la entrada, a, x, del componente ExtendedMath, una señal a es generada y enviada al componente BasicMath. Cuando el resultado está listo, otra señal tmp es generada y enviada al componente ExtendedMath. Finalmente el resultado ax es generado en la salida poco después de que etmp.  Una pregunta interesante es entonces: si el diseño del software se puede especificar en una manera similar al diseño del IC, ¿por qué no puede un sistema de software alcanzar el mismo nivel de la modularidad que en diseño del IC?

La pregunta más interesante aquí es entonces: Si el comportamiento del software se puede describir exactamente igual que el hardware, ¿por qué el software no puede alcanzar tan buena modularidad como el hardware y además no sufre el fenómeno de los subsistemas de hyperspaghetti1?

1 El fenómeno de los subsistemas de hyperspaghetti se refiere a la situación en la cual los módulos en el sistema están tan firmemente acoplados que es imposible extraer algunos de los módulos del código para su reutilización o eliminar errores de un módulo del código sin afectar a otros módulos. Ver el artículo [Bruce F. Webster, "Pitfalls of Object-Oriented Development," M&T Books, New York, 1995] para más detalles.

Conexión de Componentes

Creemos que la razón por la que el diseño del software no puede alcanzar el mismo nivel de modularidad que el diseño del IC es porque el paradigma de programación OO es fundamentalmente diferente del diseño del hardware en la conexión de sus componentes. En el paradigma de programación OO, una clase hace referencias directas a otras instancias de clases y hace llamadas a las funciones expuestas por otras instancias de clase, e.g., ExtendedMath contiene una instancia de BasicMath.  Esto implica:

  1. La conexión está oculta en el interfaz del componente (es decir, la definición de un componente). Uno no puede ver, por ejemplo, que exp_a () haga uso de ln () y de exp () solamente mirando la definición de la función. Uno debe mirar en el código para realizar la dependencia y la interacción entre llamadores y llamados.
  2. La conexión es demasiado fuerte en el sentido de que el llamador (exp_a ()) tiene que saber los nombres exactos de los llamados (ln () y exp ()). La información permite hacer la comprobación de tipos en tiempo de compilación pero también introduce enlaces innecesarios a los módulos del software. Por consiguiente, el código del software es propenso al fenómeno de los subsistemas de hyperspaghetti, y es difícil ser reutilizado en diversos contextos.

Debido a las características antedichas, es difícil desarrollar y mantener un sistema de software OO con una colección grande de funciones y de clases. En el periodo de depuración, uno no puede obtener una vista clara de las relaciones de conexión sin adentrarse en los detalles de la implementación y las líneas de código línea por línea.  Esto produce una imprevisión en el desarrollo del software y el alto coste de mantenimiento, y se llama generalmente como crisis del software.

Separación del Contrato de Conexión de la Conexión del Componente

La conexión del componente es por tanto el problema. Entonces, ¿cómo está hecho en el diseño del IC? En el diseño del IC, las señales que fluyen hacia dentro y hacia fuera del chip del IC están especificadas en la definición de interfaz. Es decir, en fase de diseño, un IC está limitado a un cierto contrato (o en la jerga del diseño del IC, la especificación del IC en el libro de especificaciones), en vez de estas limitado a los componentes que interactúan con el. La conexión del componente es distinta al momento en que un sistema(e.g., ALU) esa siendo construido.

Un contrato especifica cómo un iniciador (llamador) y un reactor (llamado) realiza cierta función.  Especifica simplemente la causalidad del intercambio de información entre los componentes pero no los componentes que pueden participar en el intercambio de información. Dos componentes, actuando respectivamente como el iniciador y el reactor, están limitados en la fase de integración del sistema para satisfacer el contrato. Un sistema con todos los componentes limita a otro a que sea completo, si los iniciadores de todos los contratos implicados se satisfacen.

En el ejemplo anterior, si exp_a (), ln () y exp () se encapsulan como componente, el componente exp_a entonces está limitado a tres diversos contratos: el cálculo de ax, de ln (x) y de e x respectivamente y es un reactor de un contrato de ax y de un iniciador de los otros dos. Semejantemente, el componente ln está limitado al contrato ln (x) y el componente exp al contrato ex, ambos como reactores.  El concepto se ilustra en la figura 2, abajo.

Figura 2: Tres componentes (exp_a, ln y exp) y los contratos están limitados. Las líneas gruesas entre los contratos indican que los contratos están emparejados el uno al otro.  Los tres componentes forman un sistema completo con los contratos de ln () y ex satisfechos, dejando el contrato de ax incumplido con un iniciador que falta. Observa que el componente ln o exp también está considerado como un sistema completo.

 

Nosotros reclamamos que los contratos de conexión en la fase de diseño y los componentes en la fase de construcción del sistema eliminen el fenómeno del hyperspaghetti. La información necesitada para atar los contratos se define en el interfaz de un componente.  De esta manera, la interconexión de componentes está bien especificada y los programadores no tienen que indagar en los detalles de la implementación para encontrar esa información. Esto evita que el fenómeno del hyperspaghetti suceda a nivel del componente y permite la composición de componentes de una manera muy similar al diseño del IC.

Interfaz en RPC, CORBA o COM/COM+ es similar al contrato en nuestra discusión, pero el interfaz no es tan flexible como el contrato y la función real que ata en esos estándares no es tan directo como en una arquitectura basada en componentes.

Con la discusión antedicha como motivación, presentaremos a continuación la arquitectura de componentes autónomos (ACA).

 

2 La Arquitectura de Componentes Autónomos(ACA)

2.1 Componentes y puertos

En la arquitectura de componentes autónomos, una entidad básica es un componente. Cada componente posee uno o másendpoints, llamados puertos. El componente donde reside un puerto se llama el componente anfitrión del puerto. Dos componentes están conectados por sus puertos de una manera permanente.  Cuando un componente envía datos a uno de sus puertos, el puerto retransmite los datos a los puertos que están conectados a él. Cuando los datos llegan a un puerto, el componente que posee el puerto procesa los datos inmediatamente en un nuevo contexto de ejecución (thread) y puede generar salidas en ciertos puertos según lo especificado en el contrato.

La salida y la entrada de un puerto están separadas por cables. Cuando los datos se envían a un puerto, se envían por el alambre de salida del puerto y llegan a los puertos por su alambre de la entrada. La figura 3 representa posibles cableados en ACA. Particularmente en (c), el alambre azul es el alambre de salida del puerto A, C y D, y el alambre de la entrada de B, de D y del E. El alambre rojo es el alambre de salida del puerto B y E, y el alambre de la entrada de C. Por ejemplo, los datos enviados por el puerto A llegarán al puerto B, D y el E. ACA no permite el lazo de envío a uno mismo. Por ejemplo, los datos enviados en el puerto D llegarán al puerto B y E, pero no a D así mismo.

Figura 3: Posible cableado entre puertos.

 
(a) Uno a uno. (b) Uno a muchos. (c) Muchos a muchos.  

Un puerto se puede conectar con otro puerto de manera simplex o de manera duplex. De la manera simplex, el alambre de salida del primer puerto se ata con el alambre de la entrada del segundo puerto o decimos que los dos alambres están unidos por la conexión. De la manera simplex, el alambre de la entrada del primer puerto y el alambre de la salida del segundo puerto se unen también. La figura 4 demuestra un ejemplo del cableado duplex.

Figura 4: Un ejemplo del cableado duplex. Conectando el puerto B (o E) y C en (a) resulta al unir el alambre azul marino y del alambre azul así como el alambre rojo oscuro y el alambre rojo, según las indicaciones de (b).

(a)

(b)


 

2.2 Contrato

Un contrato especifica cómo un iniciador (llamador) y un reactor (llamado) satisface cierta tarea. Particularmente, especifica la causalidad de los datos enviados/recibidos entre los componentes pero no los componentes que participan en la comunicación. Los contratos se pueden clasificar más a fondo en dos categorías: contrato del puerto y contrato del componente.  Un contrato del puerto está limitado específicamente a un puerto de un componente, mientras que un contrato de un componente describe cómo un componente responde a los datos que llegan a cada uno de sus puertos (e.g., cómo el componente procesa los datos, ciertas estructuras de datos son actualizadas, y generan salidas en ciertos puertos).

2.3 Componente compuesto

ACA también soporta la noción de componente compuesto. Un componente se puede componer de varios componentes y el sistema entero forma una jerarquía de componentes.  Un componente compuesto es el componente padre de los componentes encapsulados, que se llaman a su vez componentes hijos. El componente compuesto permite organizar un sistema de software con una granularidad deseable.

La figura 5 ilustra cómo el sistema de tres componentes mostrado en la Figura 2 se puede organizar en un componente compuesto. Observar que el componente compuesto tiene un puerto A conectado con el puerto B del componente encapsulado exp_a. Cuando los datos llegan al puerto A, llegan realmente al puerto B. Semejantemente, cuando los datos se envían al puerto B, se envía por el puerto A al exterior del componente exp_a. Tal puerto en un componente compuesto se llama puerto en la sombra. Lo que sucede realmente aquí es que los puertos A y B comparten el mismo alambre de salida y el mismo alambre de entrada. Estar conectado a un puerto en la sombra, implica estar conectado a los puertos de los componentes encapsulados dentro del componente del puerto a la sombra que estamos conectados.  Un comportamiento sería que un componente padre no reciba datos de un puerto en la sombra y no debe enviar datos a través de un puerto en la sombra.  El comportamiento no se define si un componente padre envía a través de un puerto en la sombra.

 

Figura 5: Encapsulación del sistema de tres componentes en la Figura 2.

 

2.4 Puerto del servidor

Un componente puede proporcionar un servicio común en uno de sus puertos a otros componentes en el sistema. Cuando un componente envía una petición a este componente para un servicio, idealmente, el componente realiza el servicio y devuelve una contestación al componente que hizo la petición. Sin embargo, la arquitectura introducida en gran medida hace que la respuesta llegue a todos los componentes que estén conectados al mismo puerto, que no es el comportamiento deseado. Para superar este problema, ACA define un tipo especial de puerto, llamado puerto del servidor.

Formalmente, un puerto del servidor está para que un componente proporcione un servicio común a otros componentes, y, específicamente, devuelva una respuesta solamente al componente que hizo la petición, sin importar que muchos componentes estén conectados con este puerto. Además, ACA especifica que el servicio esté guiado, y la respuesta se envíe, en el mismo contexto que la petición que se envió.  Uno puede decir que el enviar la petición queda “bloqueado” hasta que la contestación retorna.

El puerto A del componente exp_a en la Figura 5 es un ejemplo típico de un puerto del servidor. Imagina que dos componentes están conectados al puerto A para el servicio de calcular ax. Un componente envía a=2, x=4 y exp_a debería contestar 16 solamente al componente de petición en lugar de a ambos componentes.

2.5 Cómo se identifican los componentes y los puertos

Cada componente y cada puerto en un sistema de software tienen que ser identificados únicamente. Como la jerarquía de componentes en la arquitectura de componentes autónomos es similar al sistema de ficheros en un sistema operativo moderno, nosotros adoptamos un método de nombramiento similar a ése en el sistema de ficheros de UNIX.  Es decir, un componente es identificado por el path formado recurrentemente concatenando la trayectoria del componente de padre, un separador “/”, y la identificación del componente.

Un sistema de software forma una jerarquía de componentes consigo mismo como raíz. El componente de la raíz tiene el path “/”. Cada componente y puerto en la jerarquía pueden entonces ser identificados únicamente. Suponed que el componente padre en la Figura 5 es identificado por el path

	/prefijo/exp_a

Entonces el path del componente hijo exp_a será

	/prefijo/exp_a/exp_a

Los puertos en un componente se categorizan más a fondo en diversos grupos. Cada grupo tiene una identificación única en un componente. Cada componente tiene un grupo de puertos por defecto con una identificación nula. La identificación de un puerto en un componente anfitrión es el encadenamiento de la identificación del puerto, de un separador “@” y de la identificación de grupo. La trayectoria de un puerto se define semejantemente como el encadenamiento de la trayectoria de su componente anfitrión, “/” y su identificación en el componente. Por ejemplo, si el puerto A en la Figura 5 está en el grupo de puertos por defecto y el puerto B está en el grupo “a_x”, entonces el path del puerto A es

	/prefijo/exp_a/A@

y el path del puerto B es

	/prefijo/exp_a/exp_a/B@a_x

Nota que el separador “@” usado en la identificación de un puerto se asegura de que un componente hijo de un componente compuesto se puede distinguir de un puerto del mismo componente compuesto.

2.6 Exportando Información en Tiempo de Ejecución

Para los propósitos de diagnosis y configuración, un componente en ACA puede importar/exportar información en tiempo de ejecución a través de varios puertos señalados. La arquitectura define un puerto para tal uso, llamado puerto de información en un componente para exportar la información de diagnosis en tiempo de ejecución.   También, un componente se puede equipar de uno o más puertos de eventos, que exportan un tipo específico de eventos en tiempo de ejecución. Describimos estos puertos y los formatos de la información exportada más adelante.

Puerto de Información

Cada componente se equipa de un puerto de información, llamado puerto de información. Cuatro tipos de información se pueden exportar espontáneamente en este puerto: mensaje de error, mensaje de basura, mensajes de depuración y mensajes de trazas. Toda la información exportada comparte un formato similar:

  1. Sello de tiempo de la exportación (double)
  2. Path del componente/puerto (string)
  3. Datos (podría ser cualquier tipo)
  4. Descripción detallada (string)

Los cuatro tipos de información que pueden ser exportados se enumeran abajo.

Mensaje de error Exportado cuando un componente no puede manejar datos entrantes
(probablemente porque los datos no pueden ser reconocidos)

Formato:  1. Tiempo en que ocurre el error (double)
              2. Path del puerto a donde los datos llegaron (string)
              3. Los datos entrantes (podría ser cualquier tipo)
              4. Información de la implementación (string)
                  (por ejemplo el lugar en donde se detecta el error)
              5. Descripción detallada (string)
Mensaje de basura Exportado cuando un componente desecha datos
(probablemente porque se alcanza el límite de la capacidad o se viola cierta política)

Formato:  1. Tiempo en que se desecha el mensaje (double)
              2. Path del componente donde se desechan los datos (string)
              3. Los datos desechados (podrían ser de cualquier tipo)
              4. Descripción detallada (string)
Mensaje de depuración Exportado cuando el escritor del componente quisiera exportar información de depuración

Formato:  1. Tiempo en que se exporta el mensaje (double)
              2. Sobre qué componente o puerto se realiza en mensaje (string)
              3. Información detallada (string)
Mensaje de traza Es un mensaje especial de depuración y se exporta para todos los datos entrantes y salientes

Formato:  1. Tipo de la traza “DATA” (entrante) o “SEND” (saliente) (string)
              2. Tiempo en que se registra el mensaje (double)
              3. Path del puerto en donde los datos llegan o salen (string)
              4. Los datos (podrían ser de cualquier tipo)
              5. Información detallada (string)

Mensaje de Evento y Puertos de Eventos

Además de la información antedicha que se puede exportar en puerto de información , un componente puede también exportar acontecimientos en los puertos señalados, llamados puertos de eventos. El formato de un mensaje de evento es: 

Mensaje del acontecimiento Formato:   1. Tiempo en que se exporta el mensaje (double)
              2. Path del puerto en el cual se exporta el mensaje (string)
              3. Nombre del evento (string)
              4. Objeto del acontecimiento (podría ser de cualquier tipo)
              5. Información detallada (string)

Control de la Información Exportada

Uno puede pedir específicamente a un componente, o no, exportar ciertos tipos de información. Esto es hecho enviando un flag de 6 bits en el puerto de información de un componente. El primer bit del flag es un indicador binario que especifica permitir o inhabilitar exportando los tipos especificados de información. Los bits restantes forman una máscara que especifica qué tipo de información se solicita. El siguiente diagrama da un ejemplo de habilitación de mensajes de basura y de depuración.

Primer bit 1 Acción (encendido o apagado)
  0 Mensaje de error
  1 Mensaje la basura
  1 Mensaje de depuración
  0 Mensaje de traza
  0 Mensaje de exportación

Propiedad del componente

Cada componente puede exponer una colección de características.  Una característica de un componente es definida por un par con un nombre y un valor.  Uno puede preguntar todas las características de un componente, enviando una señal nula o una señal que contenga el nombre de la propiedad en el puerto de información del componente.  El componente entonces contesta un mensaje con la propiedad que consiste en su valor.

3 Características de la Arquitectura de Componentes Autónomos

  1. Modelo de componente débilmente acoplado: La característica más notable de la arquitectura de componentes autónomos es su modelo de componente débilmente acoplado: los contratos están limitados en la fase de diseño y los componentes están limitados en la fase de integración del sistema.  Con la separación del contrato en la conexión del componente, un componente se puede poner en ejecución individualmente y ser probado independiente del resto del sistema, se puede desplegar incremental en un sistema, y se puede extraer fácilmente de un sistema para su reutilización.
  2. Ejecución independiente del contexto para manejar datos: Otra característica importante de la arquitectura es su capacidad para manejar datos en contextos independientes de la ejecución --- en cualquier momento cuando los datos llegan a un puerto de un componente, el componente procesa los datos inmediatamente en un contexto independiente de la ejecución. La interferencia entre diversos pedazos de datos manejados por el mismo componente al mismo tiempo es mínima. Por lo tanto, un escritor de un componente no tiene que ser referido a la orden en la cual es diferente, pero los datos llegados simultáneamente son guiados.  La única condición en la cual un escritor de un componente tiene que prestar atención es cuando diversos contextos de la ejecución tienen acceso a un ciertos datos compartidos. La sincronización entre diversos contextos se debe asegurar para mantener la integridad de datos compartidos.  
  3. Diagnosis y Depuración a nivel de Componente: Según lo dicho en la sección de Exportando Información en Tiempo de Ejecución, cada componente se equipa con un puerto de información (puerto de información) y le pueden ser designados eventos de información. Uno puede conectar un componente “instrumento” con el puerto de información y los puertos de eventos de un componente para examinar, o recoger la información del componente en tiempo de ejecución. Esto imita al proceso de depuración y pruebas del IC.
  4. Realización del diseño del software del IC: La capacidad de manejar datos en contextos independientes de la ejecución, junto con el hecho de que los componentes están débilmente acoplados y limitan solamente a uno u otro en la fase de integración/implementación del sistema, permite a un componente ser autónomo. Un componente se puede por lo tanto diseñar, probar, y reutilizar en otros sistemas de software con el mismo contexto del contrato, más o menos de la misma forma en la que un chip con un IC hace en el diseño del IC. Por otra parte, con los puertos incorporados de información y de eventos disponibles, uno puede probar/depurar errores de un sistema de software a nivel del componente (mejor que en el nivel de mirar línea por línea del código). Una analogía entre la arquitectura de componentes autónomos y el diseño del IC se ilustra en la Figura 6.

Figura 6: Analogía entre un chip con un IC y un componente.

				

 

4 J-Sim

J-Sim es una implementación de ACA en Java. La razón de elegir Java como el lenguaje de programación es debido a muchas de sus características deseables, tales como independencia de la plataforma, la orientación a objetos pura, la sintaxis limpia del lenguaje, la incorporación de ejecución de threads, la capacidad de reflexión de Java, y la recolección automática de basura en tiempo de ejecución, que hacen la realización de ACA más fácil.

El desafío más grande en el desarrollo de J-Sim es proporcionar eficientemente contextos independientes de la ejecución o threads para que los componentes manejen datos de entrada. J-Sim introduce una hebra de gestión en background llamada runtime que es la clave del funcionamiento de J-Sim. En las siguientes secciones, veremos el concepto de runtime en general para más adelante estudiar el runtime utilizado en J-Sim.

La simulación es implementada en J-Sim como una extensión del runtime. Particularmente, el tiempo global de la simulación es observado por todos los threads activos en vez de que cada thread guarde un eje local del tiempo como el proceso lógico (LP) en paralelo y la simulación distribuida basada en eventos. Los detalles de cómo se hace la extensión se proporcionan en la sección de la Simulación de Tiempo Real Basada en Procesos.

4.1 Tiempo de Ejecución

Para proporcionar contextos independientes de la ejecución para los datos que llegan a diversos puertos de un componente en la arquitectura de componentes autónomos, es necesario un soporte especial en tiempo de ejecución. Particularmente, el runtime (en general) 2 tiene que crear un nuevo contexto de ejecución cuando los datos llegan al puerto de un componente.  Representamos el proceso en la figura 7, y resumimos los pasos a continuación:

  1. El componente C1 envía datos por uno de sus puertos al componente C2 en el contexto de la ejecución x.
  2. Para hacer que C2 reciba los datos, el contexto de ejecución x envía (en tiempo de ejecución) los datos y la petición de un nuevo contexto y.
  3. El runtime asume el control del proceso emisor. El contexto de ejecución x devuelve desde el proceso emisor y actúa en el componente C1.
  4. El runtime crea y activa el contexto de ejecución y para procesar los datos en el componente C2.

2Runtime es una colección de procesos en background (ocultos para las aplicaciones), como el recolector automático de basura en los lenguajes de programación modernos.

Figura 7: Cómo el runtime maneja los datos de llegada.

 

Observa que el runtime tiene control completo para crear nuevos contextos de ejecución. Para asegurarse de que el sistema de software funcione de una manera controlada, el runtime impone un límite superior al número de contextos de la ejecución que pueden ser activos simultáneamente. Cuando se alcanza el límite superior, el runtime se retrasa para crear cualquier nuevo contexto hasta que un cierto contexto existente termine su ejecución. Esto previene un número excesivo de contextos que puede agotar recursos del sistema, o en el peor caso, finalizando el sistema inesperadamente.

En J-Sim, los contextos de ejecución son implementados por los threads de Java, con el planificador de threads en la Máquina Virtual de Java(JVM).  En la actual implementación, el tiempo de ejecución se compone de dos clases, WorkerThread y ACARuntime:

  1. El WorkerThread envuelve al thread de Java con la información del contexto de ejecución. 
  2. La clase ACARuntime gestiona la creación y reciclado de los WorkerThreads así como la implementación del mecanismo de control del número de WorkerThreads que pueden estar simultáneamente activos.

Cuando los datos se envían a un puerto, el WorkerThread crea realmente una tarea y se la pasa a ACARuntime (como en el paso 2, arriba). Despues del acuse de recibo de la tarea, ACARuntime crea/despierta un WorkerThread para la tarea, o pone la tarea en la cola de procesos preparados hasta que un WorkerThread esté listo y la utilice.
 

4.2 Coste de la Comunicación Entre Componentes

Según lo mencionado arriba, cuando los datos se entregan a un componente, el tiempo de ejecución (ACARuntime) crea un thread como el nuevo contexto de ejecución para procesar los datos en el componente de recepción. Los gastos indirectos del tiempo incurridos en en este proceso reflejan los gastos indirectos de la comunicación entre componentes. El factor que contribuye a la parte principal de los gastos indirectos es cómo se crean y se manejan los threads. Un acercamiento ingenuo es crear un thread nuevo cada vez que un nuevo contexto de ejecución es necesario.  Puesto que es generalmente costoso crear y arrancar un thread nuevo, este acercamiento no se realiza correctamente. En vez de crear los thredas de nuevo, ponemos en ACARuntime un pool de threads en el que los threads son reciclados después de su ejecución y se mantienen vivos (pero dormidos). Siempre que sean necesarios, los threads se despiertan para servir como nuevos contextos de ejecución.

Para mejorar aún más el funcionamiento, permitimos a un thread anunciar su preparación (en tiempo de ejecución) antes del final de su ejecución. Es bastante común que un componente envíe un cierto resultado al final del procesado de datos. Puesto que el thread en el componente que envía será reciclado después de que acabe el procesado de los datos, es natural hacer que este thread continúe sirviendo en el componente de recepción. Para poner esto en ejecución en tiempo de ejecución, el thread en el componente emisor debe notificar ACARuntime por adelantado de modo que ACARuntime no cree/despierte otro thread para procesar los datos en el proceso emisor. Cuando el thread finaliza en el componente emisor, después obtiene los datos de ACARuntime y sirve como nuevo contexto de ejecución en el componente de recepción.  Esto, en un cierto sentido, alcanza el paradigma “un thread por mensaje según lo recomendado en la puesta en práctica de la implementación del x-kernel. 

4.3 Simulación de Tiempo Real Basada en Procesos

La simulación es implementada como una extensión del runtime en J-Sim. Básicamente, se asegura de que el sistema esté siempre ocupado (con WorkerThreads activos) manipulando cuidadosamente el tiempo de la simulación. Particularmente, maneja el tiempo de la simulación como sigue:

  1. Los pasajes de tiempo de la simulación son proporcionales al tiempo de reloj del sistema si por lo menos un WorkerThread está activo.
  2. Cuando ninguno de los WorkerThreads estan activos, el tiempo de la simulación avanza al punto “futuro” más cercano en el que por lo menos un WorkerThread puede ser despertado y llegar a activarse.
  3. Si no existe ningún WorkerThread, entonces la simulación se detiene.

Para alcanzar esto, guardamos tres variables:

El tiempo actual de la simulación se calcula entonces de la siguiente manera:

	current_simulation_time = (current_wall_time - last_time_updated)/time_scale + time_advances;

Cuando el tiempo de la simulación avanza, se ponen al día las variables:

	time_advances += nearest_simulation_future_time - current_simulation_time;

last_time_updated = current_wall_time;

Con el mecanismo antedicho, una simulación funciona de manera semejante a un sistema real, en el sentido de que los eventos de ejecución son realizados en tiempo real en comparación a los puntos fijos de tiempo en simulaciones de eventos discretos (por eso es llamada simulacion de tiempo real basada en procesos).  Las interacciones y las interferencias entre eventos, por lo tanto ocurren naturalmente como en sistemas verdaderos.  Cuando no hay threads actualmente activos, el tiempo de ejecución realiza una operación de avance de tiempo al futuro más cercano en el cual por lo menos un thread puede ser activado. Esto preserva el comportamiento de sistemas verdaderos, y por lo tanto realza la fidelidad de la simulación, ya que siempre guarda el estado de la simulación.

La variable time_scale desempeña un papel importante en tal metodología de simulación.  Por ejemplo, si un thread duerme durante 1 microsegundo, la simulación duerme realmente durante (1.0e-6 * time_scale) segundos del tiempo del sistema. Dando un valor apropiado al time_scale, por ejemplo, 1.0e6, el tiempo real del sueño llega a ser factible en la resolución del tiempo de la computadora en la cual la simulación corre. Por otra parte, podemos ajustar el time_scale de modo que el proceso retrase un acontecimiento en caídas de la simulación dentro de la gama que deseamos modelar. Por ejemplo, si el proceso real retrasa un acontecimiento 1 segundo en promedio, y nosotros deseamos modelar el proceso retrasandolo 1 milisegundo, entonces nosotros fijamos el time_scale = 1.0e3.

Observar que la simulación basada en eventos de tiempo discretos es un caso especial de la simulación de tiempo real basada en procesos con time_scale=infinito (todos los retardos de procesos llegan a ser cero en la simulación).

~ FIN ~