Tema 04 – Programación Concurrente
Programación Concurrente
Cuando se hace uso del término concurrencia, lo que éste indica es un paralelismo potencial. En
cuanto a la programación concurrente, son las técnicas de programación y sus notaciones, las
cuales permiten:
▪ Expresar paralelismo potencial
▪ Resolver problemas de sincronización y comunicación
La implementación del paralelismo en los sistemas informáticos es independiente de la
programación concurrente, la cual proporciona al programador un marco abstracto para
estudiar el paralelismo o expresar actividades lógicamente paralelas, sin necesidad de tener en
cuenta su implementación.
Con la concurrencia se permite modelar el paralelismo que vemos en el mundo real.
Prácticamente todos los sistemas en tiempo real son concurrentes, ya que los dispositivos
funcionan en paralelo con todas las acciones que ocurren en el mundo real.
Motivaciones para la escritura de Programas Concurrentes
1. Modelar el paralelismo existente en el mundo real
2. Poder usar plenamente el procesador
3. Permitir el uso de más un procesador para resolver un problema
Las plataformas de hardware modernas constan, habitualmente, de varios procesadores, por lo
que un programa concurrente es capaz de explotar este paralelismo y conseguir, por ello, una
ejecución más rápida.
Los programas concurrentes son más claros, sencillos y elegantes que sus versiones
secuenciales.
Mecanismos básicos de la Ejecución Concurrente
Dichos mecanismos consisten o persiguen:
▪ Expresión de actividades concurrentes a través de tareas e hilos
▪ Sincronización entre actividades concurrentes
▪ Primitivas de soporte a comunicación entre actividades concurrentes
Los mecanismos que se emplean para representar la ejecución concurrente son:
▪ fork/join
Son instrucciones que permiten crear tareas dinámicamente, y proporcionan un
mecanismo para pasar información a la tarea mediante parámetros.
Se basa en la instrucción fork, la cual especifica que una rutina debería ejecutarse
concurrentemente con el invocador. Con la instrucción Join, se consigue que el
invocador espere a la finalización de la rutina invocada.
Normalmente, al terminar la tarea hijo, devuelve un solo valor. Aunque son flexibles,
estas instrucciones no proporcionan una aproximación estructurada a la creación de
procesos, y su utilización es propensa a errores.
Lenguajes como Mesa o UNIX/POSIX implementan estas instrucciones.
▪ Cobegin
Es un método estructurado empleado para adelantar la ejecución concurrente de un
conjunto de instrucciones.
Permite la ejecución concurrente de instrucciones, debiendo ser cada una de ellas una
sentencia válida del lenguaje. Cobegin finaliza cuando han terminado su ejecución todas
las instrucciones concurrentes.
Si se invoca algún procedimiento, se podrán pasar datos a los procesos mediante los
parámetros de llamada.
Cobegin puede incluir, a su vez, una secuencia de instrucciones donde puede aparecer
a su vez Cobegin, construyendo así una jerarquía de procesos (este método no es muy
usado en la actualidad).
▪ Declaración explícita de Tareas
Proporcionan la posibilidad de que las propias rutinas secuenciales son las que
establecen su ejecución concurrente, haciendo más claro y fácil de entender la
estructura de un programa concurrente. Esto se consigue con la declaración explícita de
tareas.
Todos los procesos declarados dentro de un bloque comienzan a ejecutarse
concurrentemente al final de la parte declarativa de dicho bloque.
Algunos lenguajes, como ADA, permiten la creación explícita de tareas.
Alternativas a los Programas Concurrentes
La alternativa sería usar programas secuenciales, en los que el programador debe construir el
sistema, de forma que, se haga una ejecución cíclica que permita manejar las diversas
actividades concurrentes.
Es una tarea compleja que supone considerar estructuras que son irrelevantes para el control
de las actividades que se tienen que hacer.
Los programas resultantes son menos claros y pocos elegantes, siendo más compleja tanto la
descomposición del problema como su ejecución en más de un procesador.
Programa Secuencial VS Programa Concurrente
▪ Los programas secuenciales tienen un solo hilo de control. Son por ejemplo programas
escritos en C o en Fortran. Contiene por tanto un solo camino para una ejecución.
▪ Los programas concurrentes consisten en un conjunto de procesos secuenciales
autónomos que se ejecutan en paralelo, por lo que tiene diferentes caminos de
ejecución.
▪ Todos los lenguajes de programación concurrente incorporan, de forma implícita o
explícita, el concepto de proceso, donde cada proceso tiene su propio hilo de control.
La ejecución de un programa concurrente no es tan directa como la de uno secuencial.
El Sistema de Soporte (RTSS – RunTime Support System)
▪ Posee muchas de las características del planificador de un Sistema Operativo
▪ Está ubicado entre el hardware/SSOO y el software de la aplicación
▪ Puede tomar distintas formas, entre ellas:
➢ Como una estructura de programa, que es parte de la aplicación (C o C++).
➢ Como un sistema software estándar, generado con el código objeto del
programa (Ada o Java).
➢ Como una estructura hardware microcodificada en el procesador (aJile System
aJ100).
▪ Tiene que disponer de algún algoritmo de planificación que decidan las tareas que
tienen que ir ejecutándose. No obstante, en un programa bien construido, su ejecución
lógica no depende de la planificación.
Proceso
Es un programa en ejecución o una instancia de un programa que está siendo ejecutado por uno
o varios hilos. Cada vez que se ejecuta un programa, el sistema crea un proceso asociado con
este. En muchas ocasiones, referirse a este término es para indicar una o más tareas que se
ejecutan dentro de su propio contexto de memoria compartida.
Los procesos deben ser creados y finalizados, así como distribuidos, entre los procesadores
disponibles. Esto es efectuado por los sistemas de soporte en tiempo real o por el núcleo de
ejecución (RTSS - Runtime Support System).
Hilos / Hebras
Un hilo es la secuencia más pequeña de instrucciones programadas que un planificador puede
gestionar de forma independiente. Este término se emplea en muchas ocasiones para
representar un solo hilo de control.
En Java se permite la creación de hilos mediante la clase java.lang.Thread, permitiéndose
ejecutar código en un hilo independiente, proporcionando mecanismos de sincronización entre
hilos.
Java además proporciona la interfaz Runnable para la creación de hilos, permitiendo con ello
que cualquier clase que implemente esta interfaz pueda realizar una ejecución concurrente,
debiendo proporcionarse la declaración del método run.
Identificación de los Hilos en Java
Java adopta el modelo de objeto activo mediante el uso de su clase Thread.
▪ Hilo es subclase de Thread → Thread IDhilo;
▪ Objeto que proporciona el código → Runnable IDcodigohilo;
▪ Hilo en ejecución → currentThread
Terminación de Hilos en Java
▪ Al finalizar la ejecución del método run
➢ Con normalidad
➢ A través de una excepción no controlada
▪ Mediante el método stop: se persigue liberar los bloqueos sobre los objetos, y es el
encargado de ejecutar las cláusulas finally. Es inherentemente inseguro, así como está
marcado como obsoleto (no debería ser usado).
▪ Mediante el método destroy: hace que la ejecución del hilo termine directamente.
También se considera como obsoleto.
Tipos de Hilos en Java
▪ Usuario (User)
▪ Demonios (Daemon): proporcionan servicios y, normalmente, no terminan nunca.
Finalizan cuando no quedan hilos de usuario. El método setDaemon debe ser invocado
antes de iniciar el hilo.
Excepciones asociadas a los Hilos en Java
▪ IllegalThreadStateException: tanto el método start como setDaemon son invocados
después de que inicie el hilo.
▪ InterruptException: El hilo que invocó el método join es despertado porque ha sido
interrumpido, y no por la finalización del hilo dependiente.
Estados de un Hilo en Java
Tareas
Una tarea es un hilo de ejecución independientemente que pueda competir con otras tareas
concurrentes por el tiempo de ejecución del procesador. Este término se emplea en muchas
ocasiones para representar un solo hilo de control.
La noción de tarea es común a todos los lenguajes concurrentes, aunque presentan diferencias
en los siguientes niveles:
▪ Estructura
➢ Estática: nº de procesos fijo y conocido en tiempo de compilación
➢ Dinámica: nº de procesos variables y conocido en tiempo de ejecución
▪ Nivel de Paralelismo
➢ Anidado: Las tareas se definen a cualquier nivel
➢ Plano: Todas las tareas tienen el mismo nivel
▪ Granularidad
➢ Fina: contiene muchas tareas, muchas de ellas de poca duración
➢ Gruesa: contiene pocas tareas, la mayoría de ellas de larga duración
▪ Inicialización: Se le da información relacionada con su ejecución, por lo que se le puede
pasar información como:
➢ Paso de Parámetros
➢ IPC (Interprocess Communication): Se le comunica una vez ha comenzado su
ejecución
▪ Terminación: puede realizarse por distintas razones:
➢ Finalización del cuerpo de la tarea
➢ Ejecución de una tarea del tipo auto-terminación
➢ Por ser abortada por una acción explícita de otra tarea
➢ Ocurre una condición de error no tratado
➢ Por terminar la tarea cuando ésta ya no es necesaria
➢ No termina de finalizarse por ejecutar un lazo sin terminación
▪ Representación
Cuando se tiene anidación de niveles, se puede crear una jerarquía entre las tareas, debiendo
distinguirse entre aquellas que son responsables de su creación (tienen una relación padre/hijo),
y aquellas otras que son afectadas por su terminación (tienen una relación:
guardián/dependiente).
El guardián no puede finalizar hasta que no haya finalizado todas sus tareas dependientes. En
caso de estructuras estáticas, el padre y el guardián son el mismo. En el caso de estructuras
dinámicas pueden ser diferentes.
A nivel de Java, las tareas:
▪ Permiten la creación dinámica de tareas
▪ Permite el paso de parámetros a través del constructor
▪ Creación de grupos de tareas
➢ No existe el concepto de guardián o maestro
➢ Los objetos inaccesibles son liberados por el recolector de basura
▪ El programa principal termina cuando han finalizado todos sus hilos de usuario
▪ Un hilo puede esperar a que otro finalice, invocando el método join
El método isAlive permite determinar si un hilo ha terminado o no su ejecución.
Implementación o formas de ejecución de Tareas
Puede realizarse de 3 formas diferentes, o como combinación de cualquiera de ellas, las cuales
son:
▪ Multiprogramación: Las tareas se multiplexan en un único procesador.
▪ Multiprocesamiento: Es un sistema multiprocesador con memoria compartida.
▪ Sistemas Distribuidos: Las tareas se ejecutan en varios procesadores y sin memoria
compartida.
Solamente la segunda o tercera forma supone una ejecución en paralelo.
Comportamientos de las Tareas en los Lenguajes Concurrentes
Considerando la interacción entre tareas, existen tres tipos de comportamientos:
▪ Independientes: las tareas no se sincronizan ni se comunican con las otras.
▪ Cooperativas: las tareas si se sincronizarán y comunicarán con las otras.
▪ Competitivas: son esencialmente tareas independientes. No obstante, necesitan
comunicarse y sincronizarse con otras tareas para poder utilizar recursos que son
limitados (periféricos, memoria, etc.).
Estados de una Tarea
Programación Orientada a Objetos
En los lenguajes bajo el paradigma de la programación orientada a objetos, se deben considerar
dos tipos de objetos:
▪ Activos: ejecuta acciones espontáneas, permitiendo que se realice la computación.
▪ Reactivos: solo ejecutan acciones que son invocadas por objetos activos. Los recursos
serían reactivos, aunque puedan realizar el control de sus estados internos, así como el
control de cualquier recurso real.
Tipos de Entidades o Representación de las mismas (Programación Orientada a
Objetos)
La implementación de las entidades de los recursos necesita de algún agente de control. Si este
es pasivo, se dice que el recurso es protegido o sincronizado.
Si se requiere a nivel de control, el recurso se considera, en cierto sentido, activo, denominando
a este tipo de entidades como servidor. Los tipos de entidades, en los lenguajes concurrentes,
son y se representan, como:
▪ Entidades Activas: son objetos que se representan mediante tareas o hilos internos, de
forma explícita o implícita.
▪ Entidades Pasivas: son objetos reactivos que se representan como variables o
encapsulados (módulos, paquetes o clases), sin restricciones de sincronización
(requieren de un hilo de control externo).
▪ Recursos Protegidos: son objetos reactivos que se representan encapsulados como
módulos, requiriendo un servicio de sincronización, con restricciones, de bajo nivel.
▪ Servidor/es: son objetos activos que requieren de una tarea, ya que se requiere
programar el agente de control. Tienen restricciones de sincronización
Etapas de creación de una Aplicación Distribuida
▪ Particionado: proceso de dividir el sistema en partes (o unidades de distribución)
adecuadas, para ser situadas sobre elementos de proceso del sistema en cuestión.
▪ Configuración: tendrá lugar cuando las partes en las que está particionado el programa
se encuentren ya asociadas con elementos de procesos concretos del sistema en
cuestión.
▪ Asignación: cubre el proceso real de convertir el sistema configurado en un conjunto de
módulos ejecutables y descargar estos sobre los elementos de procesamiento del
sistema en cuestión.
▪ Ejecución Transparente: es la ejecución del software distribuido, de modo que sea
posible acceder a los recursos remotos independientemente de su ubicación.
▪ Reconfiguración: es el cambio dinámico de ubicación de un componente o recurso
software.
Los lenguajes que explícitamente se conciben para abordar la programación distribuida
proporcionan soporte lingüístico, al menos en la etapa de particionado y en el desarrollo del
sistema. Con ello se proponen comunidades de distribución a los procesos, objetos, particiones,
agentes y vigilantes.
La asignación y reconfiguración requieren el apoyo del entorno de programación y del sistema
operativo.
Para la ejecución transparente se precisan mecanismos que permitan que los procesos no
tengan que tratar con la forma de bajo nivel de los mensajes.
También se tiene que presuponer que todos los mensajes recibidos por los procesos se
encuentran intactos y en buenas condiciones, que son de la clase que los procesos esperan, y
que no existen restricciones para la comunicación entre procesos con relación a los tipos
predefinidos del sistema.
Estándares de Comunicación de Programas Distribuidos
▪ API (Application Programming Interface): Interface de programación de aplicaciones,
como Sockets (conectores), usado para los protocolos de transporte de red.
▪ RPC (Remote Procedure Call): es un paradigma de llamadas a un procedimiento remoto.
Tiene como objetivo hacer la comunicación tan simple como sea posible. Se emplea,
generalmente, para comunicar programas escritos en el mismo lenguaje (por ejemplo,
ADA o Java). No obstante, podría usarse CORBA para comunicar, por ejemplos, dos
programas escritos en C++ y Java, respectivamente.
▪ Mediante el paradigma de Objetos Distribuidos.