El arte de un buen diseño es elegir la mejor forma de cubrir una serie de requisitos. El arte de un buen diseño de
sistema concurrente a menudo es elegir el modo más simple de satisfacer las necesidades de la concurrencia. Una de las
primeras normas de los diseñadores debería ser evitar volver a inventar la rueda. Se han desarrollado buenos patrones
de diseño y lenguajes de diseño para solucionar la mayoría de problemas. Vista la complejidad de los sistemas
concurrentes, sólo tiene sentido utilizar soluciones probadas y esforzarse por conseguir la simplicidad de diseño.
Las tareas concurrentes que tienen lugar enteramente dentro de un sistema, se denominan hebras de ejecución.
Como todas las tareas concurrentes, las hebras de ejecución son un concepto abstracto ya que se producen en el tiempo.
Lo mejor que se puede hacer para capturar físicamente una hebra de ejecución es representar su estado en un instante
concreto en el tiempo.
La forma más directa de representar tareas concurrentes mediante sistemas es dedicar un sistema separado para cada
tarea. Sin embargo, esto suele ser demasiado caro y no siempre contribuye en la resolución de conflictos. Es común, por
lo tanto proporcionar soporte a múltiples tareas en el mismo procesador físico a través de alguna forma de proceso
de múltiples tareas. En este caso, se comparten el procesador y los recursos asociados como memoria y buses. (Por
desgracia, compartir recursos también puede generar nuevos conflictos que no estaban presentes en el problema
original).
La forma más común de proceso de múltiples tareas es proporcionar un procesador "virtual" a cada tarea. Se suele hacer
referencia al procesador virtual como un proceso o tarea. Habitualmente, cada proceso tiene su propio espacio de
direcciones que, lógicamente, es distinto del espacio de direcciones de otros procesadores virtuales. Esto protege los
procesos de entrar en conflicto entre si y contra sobrescrituras accidentales de la memoria. Por desgracia, los gastos
generales necesarios para conmutar el procesador físico de un proceso a otro suelen ser prohibitivos. Implica el
intercambio significativo de conjuntos de registro dentro de la CPU (conmutación del contexto) que incluso con
los procesadores modernos de alta velocidad, tarda cientos de microsegundos.
Para reducir estos gastos generales, muchos sistemas operativos proporcionan la posibilidad de incluir múltiples
hebras ligeras dentro de un único proceso. Las hebras de un proceso comparten el espacio de direcciones de ese
proceso. Esto reduce los gastos generales implicados en la conmutación del contexto, pero aumenta la posibilidad de
conflictos en la memoria.
Para algunas transacciones de alto rendimiento, incluso los gastos generales de las hebras ligeras puede ser
inaceptablemente alto. En tales situaciones, es habitual tener una forma más ligera de proceso de múltiples tareas que
se alcanza aprovechando algunas características especiales de la aplicación.
Los requisitos de concurrencia del sistema pueden tener un impacto dramático en la arquitectura del sistema. La
decisión de mover la funcionalidad de una arquitectura de único proceso a una arquitectura de proceso múltiple
introduce cambios significativos en la estructura del sistema, en muchas dimensiones. Es posible que resulte necesario
introducir mecanismos adicionales (por ej., las llamadas a procedimiento remotos) que pueden cambiar sustancialmente la
arquitectura del sistema.
Los requisitos de disponibilidad del sistema deben tenerse en cuenta, así como los gastos generales adicionales de
gestionar los procesos y las hebras adicionales.
Como la mayoría de decisiones arquitectónicas, cambiar la arquitectura del proceso conlleva, efectivamente, un conjunto
de problemas para otro:
Enfoque
|
Ventajas
|
Desventajas
|
Proceso único, sin hebras
|
-
Simplicidad
-
Mensajería rápida dentro del proceso
|
-
Carga de trabajo difícil de equilibrar
-
No se puede escalar a múltiples procesadores
|
Proceso único, con múltiples hebras
|
-
Mensajes rápidos dentro del proceso
-
Proceso de múltiples tareas sin comunicación entre el proceso
-
Proceso de múltiples tareas mejorado sin los gastos generales de los procesos pesados
|
-
La aplicación debe ser de 'enhebramiento seguro'
-
El sistema operativo debe tener una gestión de hebras eficaz
-
Hay que tener en cuenta las cuestiones de memorias compartidas
|
Múltiples procesos
|
-
Se escala bien a medida que se añaden procesadores
-
Relativamente fácil de distribuir a través de nodos
|
-
Sensible a límites de proceso: utilizar demasiada comunicación entre el proceso afecta al
rendimiento
-
El intercambio y la conmutación de contexto son caros
-
Más difícil de diseñar
|
Una vía de acceso evolutiva típica es empezar con una arquitectura de proceso único, añadiendo procesos para grupos de
comportamientos que deben ocurrir simultáneamente. Dentro de estas agrupaciones más amplias, tenga en cuenta las
necesidades adicionales de concurrencia, añadiendo hebras en los procesos para aumentar la concurrencia.
El punto de partida inicial es asignar muchos objetos activos a una única tarea o hebra del sistema operativo, mediante
un planificador de objetos activos creado con este objetivo. De este modo suele ser posible alcanzar una simulación muy
ligera de la concurrencia, aunque, con una única tarea o hebra del sistema operativo, no será posible aprovechas las
máquinas con múltiples CPU. La decisión clave es aislar el comportamiento de bloqueo en hebras separadas, para
que el comportamiento de bloqueo no se convierta en un cuello de botella. Esto resultará en una separación de los
objetos activos con comportamiento de bloqueo en sus propias hebras de sistema operativo.
En sistemas de tiempo real, este razonamiento se aplica igualmente a cápsulas - cada cápsula tiene una hebra lógica de
control que puede compartir, o no, la hebra de un sistema operativo, tarea o proceso con otras cápsulas.
Por desgracia, como muchas decisiones arquitectónicas, no existen respuestas sencillas; la solución correcta implica un
enfoque cuidadosamente equilibrado. Los pequeños prototipos arquitectónicos se pueden utilizar para explorar las
implicaciones de un conjunto concreto de opciones. En la creación del prototipo de la arquitectura del proceso,
céntrese en la escala del número de procesos hasta los máximos teórico para el sistema. Tenga en cuenta las cuestiones
siguientes:
-
¿El número de procesos se puede escalar al máximo? ¿Hasta que punto máximo se puede forzar el sistema? ¿Existe
capacidad de crecimiento potencial?
-
¿Cuál es el impacto de cambiar algunos de los procesos para aligerar las hebras que operan en un espacio de
direcciones de proceso compartido?
-
¿Qué sucede con el tiempo de respuesta a medida que se añaden procesos? ¿A medida que la comunicación entre el
proceso (IPC) se aumenta? ¿Se percibe la degradación?
-
¿La cantidad de IPC se puede reducir combinando o reorganizando los procesos? ¿Este cambio producirá procesos
grandes y monolíticos cuya carga es difícil de equilibrar?
-
¿Se puede utilizar una memoria compartida para reducir el IPC?
-
¿Todos los procesos deben obtener un "tiempo equivalente" cuando se asignan los recursos de tiempo? ¿Es posible
llevar la asignación de tiempo? ¿Existen posibles inconvenientes a cambiar las propiedades de la planificación?
Los objetos activos se pueden comunicar entre si de forma síncrona o asíncrona. La comunicación síncrona es útil porque
puede simplificar las colaboraciones complejas a través de secuencias controladas estrictamente. Es decir, mientras un
objeto activo ejecuta un paso hasta su conclusión que implique invocaciones síncronas de otros objetos activos,
cualquier interacción concurrente iniciada por otros objetos se puede omitir hasta que se haya completado toda la
secuencia.
Aunque resulta útil en algunos casos, también puede ser problemático ya que puede ocurrir que un suceso más importante
de alta prioridad tenga que esperar (inversión de la prioridad). Esto se agrava con la posibilidad de que el objeto
invocado síncronamente se bloquee esperando una respuesta a una invocación síncrona de si mismo. Esto puede conducir a
una inversión de la prioridad ilimitada. En los casos más extremos, si hay circularidad en la cadena de invocaciones
síncronas, puede llevar a un punto muerto.
Las invocaciones asíncronas evitan este problema habilitando los tiempos de respuesta definidos. Sin embargo,
dependiendo de la arquitectura de software, la comunicación asíncrona suele conducir a un código más complejo ya que un
objeto activo puede tener que responder a varios sucesos asíncronos (cada uno de los cuales puede conllevar una
compleja secuencia de interacciones asíncronas con otros objetos activos) en cualquier momento. Esto puede resultar muy
difícil y ser propenso a errores de implementación.
La utilización de una tecnología de mensajería asíncrona con entrega de mensajes garantizada puede simplificar la tarea
de programación de la aplicación. La aplicación puede proseguir con la operación incluso si la conexión de red o la
aplicación remota no está disponible. La mensajería asíncrona no excluye su utilización en modo síncrono. La tecnología
síncrona requerirá que esté disponible una conexión siempre que esté disponible la aplicación. Como se sabe que existe
una conexión, el manejo de confirmación del proceso será más sencillo.
En el enfoque recomendado en Rational Unified Process para sistemas de tiempo real, las cápsulas comunican asíncronamente a través del uso de señales, de acuerdo con unos protocolos concretos. Es posible, todavía, alcanzar la comunicación
síncrona a través del uso de los pares de señales, uno en cada dirección.
Aunque los gastos generales de conmutación del contexto de los objetos activos puede ser muy bajo, es posible que
algunas aplicaciones todavía puedan encontrar que el coste es inaceptable. Esto suele ocurrir en situaciones en que
grandes cantidades da datos deben procesarse a una velocidad muy elevada. En estos casos, es posible que deba volver a
utilizar los objetos pasivos y técnicas de gestión de la concurrencia más tradicionales (pero con riesgos más elevados)
como los semáforos.
Estas consideraciones, sin embargo, no implican necesariamente que debamos abandonar totalmente el enfoque del objeto
activo. Incluso en estas aplicaciones de datos intensivos, suele ocurrir que los componentes sensibles a rendimiento
son una parte relativamente pequeña del sistema global. Esto implica que el resto del sistema todavía puede aprovechar
el paradigma de objeto activo.
En general, el rendimiento es sólo uno de los criterios de diseño en lo referente al diseño de sistema. Si el sistema
es complejo, los otros criterios como la capacidad de mantenimiento, la facilidad de cambio, la comprensibilidad, etc.,
existen igualmente o son incluso más importantes. El enfoque del objeto activo tiene una ventaja clara ya que oculta
parte de la complejidad de la concurrencia y la gestión de la concurrencia al tiempo que permite que el diseño se
exprese en términos específicos de la aplicación en oposición a los mecanismos específicos de la tecnología de bajo
nivel.
Los componentes concurrentes sin interacciones son prácticamente un problema trivial. Casi todos los retos del diseño
están relacionados con las interacciones entre tareas concurrentes, para que debamos centrar primero nuestra energía en
comprender las interacciones. Algunas de las preguntas que debe formularse son:
-
¿La interacción es unidireccional, bidireccional o multidireccional?
-
¿Existe una relación cliente/servidor o maestro/esclavo?
-
¿Es necesaria alguna forma de sincronización?
Una vez que se haya comprendido la interacción, podemos pensar en los modos de implementarla. La implementación debe
seleccionarse para producir el diseño más simple coherente con los objetivos de rendimiento del sistema. Los requisitos
de rendimiento generalmente incluyen producción global y latencia aceptable en la respuesta a sucesos generados
externamente.
Estas cuestiones son incluso más críticas para sistemas de tiempo real, que a menudo son menos tolerantes a variaciones
de rendimiento, por ejemplo, 'variaciones' en tiempo de respuesta, o con horas límite incumplidas.
Es una práctica incorrecta incorporar las suposiciones específicas sobre interfaces externas en toda la aplicación, y
es muy ineficaz tener varias hebras de control bloqueado esperando un suceso. En su lugar, asigne a un único objeto la
tarea dedicada de detectar el suceso. Cuando ocurre el suceso, ese objeto puede notificar a quienes deben conocer el
suceso. Este diseño se basa en un patrón de diseño conocido y probado, el patrón "Observador" [GAM94]. Se puede ampliar fácilmente para obtener una mayor flexibilidad con el
"Patrón Editor-suscriptor", donde el editor de un objeto actúa como intermediario entre los detectores del suceso y los
objetos interesados en el suceso ("suscriptores") [BUS96].
Las acciones en un sistema se pueden desencadenar con la aparición de sucesos generados externamente. Un suceso
generado externamente muy importante puede ser simplemente el paso del tiempo, tal como lo representa el tictac de un
reloj. Otros sucesos externos provienen de los dispositivos de entrada conectados al hardware externo, que incluye
dispositivos de interfaz de usuario, sensores de proceso y enlace de comunicaciones con otros sistemas. Esto es
predominantemente cierto para sistemas de tiempo real, que habitualmente tienen una alta conectividad con el mundo
exterior.
Para que el software detecte un suceso debe estar bloqueado esperando una interrupción, o bien debe comprobar el
hardware periódicamente para ver si se ha producido el suceso. En el último caso, el ciclo periódico puede necesitar
ser corto para evitar perder un suceso reciente o múltiples apariciones, o simplemente minimizar la latencia entre la
aparición y la detección del suceso.
El aspecto interesante sobre esto es que no importa lo inusual que sea un suceso, algún software debe estar bloqueado
esperándolo o comprobándolo con frecuencia. Pero muchos (o la mayoría) de los sucesos que un sistema debe manejar son
poco usuales; la mayor parte del tiempo, en cualquier sistema, no sucede nada relevante.
El sistema del ascensor proporciona muchos buenos ejemplos de esto. Los sucesos importantes en la vida de un ascensor
incluyen las llamadas de servicio, la selección del piso del pasajero, la mano de un pasajero que bloquea la puerta y
pasar de un piso al siguiente. Algunos de estos sucesos requieren una respuesta crítica en el tiempo, pero son
extremadamente inusuales en comparación con la escala de tiempo del tiempo de respuesta deseado.
Un único suceso puede desencadenar muchas acciones, y las acciones pueden depender de los estados de diferentes
objetos. Además, las diferentes configuraciones de un sistema pueden usar el mismo suceso de modos distintos. Por
ejemplo, cuando un ascensor pasa un piso la pantalla de la cabina del ascensor se debe actualizar y el propio ascensor
debe saber dónde está para saber cómo responder a nuevas llamadas y selecciones del piso del pasajero. Pueden haber, o
no, pantallas de ubicación del ascensor en cada piso.
Los sondeos son caros; requieren que una parte del sistema se detenga periódicamente lo que está haciendo para
comprobar si se ha producido un suceso. Si se debe responder rápidamente al suceso, el sistema tendrá que comprobar con
frecuencia la llegada del suceso, además de limitar la cantidad de otros trabajos que puede cumplir.
Resulta mucho más eficaz asignar una interrupción al suceso, con el código dependiente del suceso que se activa con la
interrupción. Aunque las interrupciones suelen evitarse porque se consideran "caras", si se utilizan juiciosamente
pueden ser mucho más eficaces que un sondeo repetido.
Los casos en que las interrupciones serían preferibles como mecanismo de notificación de sucesos son aquellos donde la
llegada del suceso es aleatoria y poco frecuente, como cuando la mayoría de esfuerzos de sondeo descubren que el suceso
no ha sucedido. Los casos en que el sondeo sería preferible son aquellos donde los sucesos llegan de forma regular y
previsible y la mayoría de esfuerzos de sondeo descubren que el suceso se ha producido. En el medio, existirá un punto
en que uno es indiferente al sondeo o al comportamiento reactivo - funcionará bien igualmente y la elección no importa.
En la mayoría de casos, sin embargo, vista la aleatoriedad de los sucesos del mundo real, es preferible el
comportamiento reactivo.
La difusión de datos (habitualmente utilizando señales) es cara, y habitualmente un desperdicio - sólo unos pocos
objetos pueden estar interesados en los datos, pero todos (o muchos) se detendrán a examinarlos. Un enfoque mejor y que
consume menos recursos es la utilización de la notificación para informar sólo a los objetos que están interesados que
se ha producido un suceso. Restrinja la difusión a los sucesos que requieren la atención de muchos objetos
(habitualmente, los sucesos de tiempo o de sincronización).
Más concretamente:
-
Utilice objetos pasivos e invocaciones de método síncrono donde la concurrencia no es un problema pero la respuesta
instantánea sí.
-
Utilice los objetos activos y los mensajes asíncronos para la amplia mayoría de conceptos de concurrencia de nivel
de aplicación.
-
Utilice hebras de sistema operativo para aislar los elementos de bloqueo. Un objeto activo puede correlacionarse
con una hebra de sistema operativo.
-
Utilice procesos de sistema operativo para obtener un aislamiento máximo. Son necesarios procesos independientes si
los programas necesitan iniciarse y apagarse independientemente, y para subsistemas que pueden ser necesarios para
su distribución.
-
Utilice CPU separadas para la distribución física o para caballos de potencia brutos.
Quizás la directriz más importante para el desarrollo eficaz de aplicaciones concurrentes es maximizar el uso de los
mecanismos de concurrencia de peso más ligero. El hardware y el software del sistema operativo juegan un papel
importante en el soporte de la concurrencia, pero ambos proporcionan mecanismos relativamente pesados, dejando una gran
parte del trabajo al diseñador de la aplicación. Nos queda salvar las distancias entre las herramientas disponibles y
las necesidades de aplicaciones concurrentes.
Los objetos activos ayudan a salvar las distancias mediante dos características clave:
-
Unifican las abstracciones de diseño encapsulando la unidad básica de concurrencia (una hebra de control) que se
puede implementar mediante cualquiera de los mecanismos subyacentes proporcionados por el sistema operativo o la
CPU.
-
Cuando los objetos activos comparten una hebra de sistema operativo única, se convierten en un mecanismo de
concurrencia ligero y muy eficaz que, si no, tendrá que implementarse directamente en la aplicación.
Los objetos activos forman también un entorno ideal para los objetos pasivos proporcionados por los lenguajes de
programación. Diseñar un esquema completamente desde la fundación de los objetos concurrentes sin artefactos de
procedimiento como programas y procesos conduce a diseños más modulares, consistentes y comprensibles.
En la mayoría de sistemas, menos del 10% del código utiliza más del 90% de los ciclos de CPU.
Muchos diseñadores de sistemas actúan como si todas las líneas de código debieran optimizarse. En lugar de eso, dedique
el tiempo a optimizar el 10% del código que se ejecuta más a menudo o que ocupa más tiempo. Diseñe el 90% restante
enfatizando la capacidad de comprensión, de mantenimiento, la modularidad y la facilidad de implementación.
Los requisitos no funcionales y la arquitectura del sistema afectarán a la elección de los mecanismos utilizados para
implementar las llamadas de procedimiento remoto. Una visión general de los tipos de renuncias entre
alternativas se presenta a continuación.
Mecanismo
|
Usos
|
Comentarios
|
Mensajería
|
Acceso asíncrono de los servidores de la empresa
|
El middleware de mensajería puede simplificar la tarea de programación de la aplicación manejando las
colas, los tiempos de espera y las condiciones de recuperación/reinicio. También puede utilizar el
middleware de mensajería en modalidad pseudosíncrona. Habitualmente, la tecnología de mensajería puede
proporcionar soporte a mensajes de gran tamaño. Algunos enfoques de RPC pueden estar limitados respecto
al tamaño de los mensajes, requiriendo programación adicional para manejar mensajes grandes.
|
JDBC/ODBC
|
Llamadas de la base de datos
|
Estas son interfaces independientes de la base de datos para servlets Java o programas de aplicación
para realizar llamadas a bases de datos que pueden estar en el mismo o en otro servidor.
|
Interfaces nativas
|
Llamadas de la base de datos
|
Muchos proveedores de bases de datos han implementado interfaces de programa de aplicación nativas a
sus propias bases de datos que ofrecen una ventaja de rendimiento sobre ODBC a expensas de la
portabilidad de aplicación.
|
Llamada a procedimiento remoto
|
Para llamar programas en servidores remotos
|
Es posible que no necesite programar en el nivel de RPC si tiene un constructor de aplicación que se
encarga de esto en su lugar.
|
Conversacional
|
Poco utilizado en aplicaciones de e-business
|
Habitualmente, la comunicación de programa a programa de bajo nivel utiliza protocolos como APPC o
Sockets.
|
Muchos sistemas requieren comportamiento concurrente y componentes distribuidos. La mayoría de lenguajes de
programación nos facilitan poca ayuda con cualquiera de estas cuestiones. Hemos visto que es necesaria una buena dosis
de abstracción para comprender la necesidad de concurrencia en las aplicaciones, y las opciones para implementarla en
el software. También hemos visto que, paradójicamente, mientras el software concurrente es inherentemente más complejo
que el software no concurrente, también es capaz de simplificar mucho el diseño de los sistemas que deben tratar con la
concurrencia en el mundo real.
|