el lenguaje c++ concurrente - .:: · pdf file3 el contenido de este artículo es como...

37
El lenguaje C++ Concurrente José Oscar Olmedo Aguirre * Centro de Investigación y de Estudios Avanzados del IPN Departamento de Ingeniería Eléctrica Sección de Computación Resumen El propósito de este trabajo es hacer una sinopsis del lenguaje C++ Concurrente desarrollado por el autor. El lenguaje C++ Concurrente es una extensión al lenguaje C++ basada en el modelo de Proceso Distribuido de Brinch Hansen [3] que hace posible la programación concurrente orientada a objetos. Además, se describen y resuelven los problemas que describe Brinch Hansen en su artículo usando el C++ Concurrente. Los programas fueron completamente probados en el prototipo del compilador desarrollado para él. Palabras clave: Programación concurrente, programación orientada a objetos, lenguajes de programación, no determinismo, clases, objetos, procesos, comunicación, sincronización, cooperación, regiones críticas condicionales, semáforos. ------------- * Profesor de la Sección de Computación del Departamento de Ingeniería Eléctrica.

Upload: truongtuyen

Post on 01-Feb-2018

222 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

El lenguaje C++ Concurrente

José Oscar Olmedo Aguirre * Centro de Investigación y de Estudios Avanzados del IPN Departamento de Ingeniería Eléctrica Sección de Computación Resumen El propósito de este trabajo es hacer una sinopsis del lenguaje C++ Concurrente desarrollado por el autor. El lenguaje C++ Concurrente es una extensión al lenguaje C++ basada en el modelo de Proceso Distribuido de Brinch Hansen [3] que hace posible la programación concurrente orientada a objetos. Además, se describen y resuelven los problemas que describe Brinch Hansen en su artículo usando el C++ Concurrente. Los programas fueron completamente probados en el prototipo del compilador desarrollado para él. Palabras clave: Programación concurrente, programación orientada a objetos, lenguajes de programación, no determinismo, clases, objetos, procesos, comunicación, sincronización, cooperación, regiones críticas condicionales, semáforos. ------------- * Profesor de la Sección de Computación del Departamento de Ingeniería Eléctrica.

Page 2: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

2

PREFACIO La programación orientada a objetos es un modelo de programación que plantea la descomposición de sistemas en un conjunto de entidades distinguibles llamados objetos, los cuales poseen un estado o configuración interna. El estado de un objeto se caracteriza por una colección de propiedades medibles que le es propia. El estado del objeto se puede transformar por la aplicación de ciertas operaciones, las cuales constituyen el único medio de interacción con el exterior. La programación concurrente es una metodología para tratar con problemas que involucran múltiples entidades activas desarrollándose con un paralelismo potencial. Las entidades activas o procesos poseen un comportamiento propio que se manifiesta por la ejecución de procedimientos lo que les permite interactuar con otros procesos. Los procesos corresponden con nuestra noción usual de programas secuenciales. La programación concurrente orientada a objetos unifica los conceptos de objeto y proceso con un enfoque más amplio. Sin embargo, de acuerdo con sus características se pueden distinguir dos tipos de objetos de acuerdo con su comportamiento en pasivos y activos. Los objetos pasivos corresponden con la noción usual de colección de atributos y operaciones. Los objetos activos extienden a los objetos pasivos hasta considerarlos auténticos programas secuenciales. Aún cuando la programación concurrente tiene sus orígenes en el estudio de los sistemas operativos es necesario distinguirlos. La tendencia es aumentar el grado de abstracción de estos conceptos hasta convertirlos en construcciones de un lenguaje de programación. Este trabajo es una recapitulación de mi tesis de maestría: el lenguaje PL/M-86 Concurrente [8], una extensión al lenguaje estructurado por bloques, PL/M-86, para incluir objetos y procesos. Dicho lenguaje fue un primer intento por conjuntarlos. Sin embargo, los objetos que se lograron implantar eran muy simples porque sólo permitían encapsular datos y procedimientos, características indispensables de los tipos abstractos de datos. En el lenguaje C++ Concurrente parto de un lenguaje orientado a objetos, el C++, el sucesor más notable del lenguaje C. El lenguaje C++ permite escribir programas con un grado de abstracción mayor, facilitando así la extensión hacia la concurrencia.

Page 3: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

3

El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección analiza el cambio de contexto entre procesos y corrutinas. Finalmente, la tercera sección presenta los ejemplos que analiza Brinch Hansen en su artículo [3] escritos en el C++ Concurrente. Deseo agradecer al M. en C. Jorge Buenabad Chávez por los valiosos comentarios y sugerencias que hizo a este trabajo.

Page 4: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

4

1. SINOPSIS DEL LENGUAJE C++ CONCURRENTE Los modelos dinámicos consideran que un sistema se encuentra formado por una colección de objetos autónomos que interactuan entre sí. Los objetos del sistema son inherentemente concurrentes y poseen un estado que puede cambiar por su interacción con otros objetos. Las interacciones ocurren cuando se usan ciertos procedimientos o servicios que ofrece o solicita el objeto. Las reglas de como se deben realizar dichas interacciones constituyen el modelo de programación en el que se basa el sistema. El modelo de Proceso distribuido tiene el mérito de su generalidad y sencillez. La generalidad se manifiesta por su capacidad para describir una extensa variedad de conceptos en forma unificada: - Procedimientos - Corrutinas - Procesos - Clases - Monitores - Semáforos - Recipientes (buffers) - Administradores de recursos - Trayectorias (path expressions) Su sencillez se debe a los pocos y eficaces mecanismos para la sincronización y la comunicación entre procesos, lo que por otra parte, simplifica enormemente la implementación. El lenguaje C++ Concurrente fué diseñado e implementado por el autor siguiendo la dirección apuntada en su Tesis de maestría [8]. El C++ Concurrente es una extensión al lenguaje C++ que difiere con el enfoque propuesto en otros trabajos como el C Concurrente de Gehani y Roome, basado en el concepto de encuentro extendido [6] una extensión del encuentro (rendesvouz) de Ada [5], ó la implantación de corrutinas hecha en los Laboratorios Bell como se describe en [13]. La diferencia esencial radica en el modelo de programación en el que se basa nuestro lenguaje. En esta sección presentaremos y discutiremos las extensiones del lenguaje. 1.0. Características del lenguaje C++ Concurrente

Page 5: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

5

Las características más relevantes del modelo en el que se basa el lenguaje C++ Concurrente se enumeran a continuación:

1. Abstracción de datos mediante clases (C++). 2. Creación y destrucción estática y dinámica de procesos y corrutinas

(extensión). 3. Topología estática y dinámica de procesos (extensión). 4. Programación en el estilo de proceso distribuido (extensión). 5. Ejecución no determinística de programas (extensión). 6. Comunicación síncrona bidireccional (consecuencia). 7. Comunicación por canales (extensión). 8. Compilación separada de módulos y facilidades de depuración

(ambiente). 9. Caracterización del sistema operativo como un proceso prestador de

servicios. El lenguaje cuenta con un preprocesador construido sobre el lenguaje C++ de Borland versión 1.0 para máquinas basadas en el 8086 en el modelo de memoria small. El prototipo aprovecha las características del lenguaje así como las de su preprocesador para introducir el no determinismo que proveen los comandos protegidos con guardias. Un hecho notable de la implantación del preprocesador es la de crear la ilusión de que se programa en un lenguaje que posee nuevas construcciones que son consistentes con el lenguaje C++ sin necesidad de usar un preprocesador externo. Aún cuando se construyen varias estructuras de datos y de control a tiempo de ejecución, el código generado posee un rendimiento bastante aceptable. Esto se debe a que las estructuras se construyen una sola vez, cuando se necesitan usar. Por otra parte, tiene la ventaja de probar las posibilidades que brinda el lenguaje antes de intentar realizar la construcción de un compilador. Esta decisión tiene consecuencias favorables que tiene sobre la facilidad de uso, la disponibilidad o la portabilidad (de los programas hechos en el C++). En esta versión no se incluye: 1. Manejo de excepciones. 2. Manejo de interrupciones. 3. Métodos de verificación de especificaciones.

Page 6: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

6

En este reporte, tampoco se consideraron aspectos tan importantes como la medida del rendimiento, las arquitecturas o las redes de computadoras para procesamiento distribuido, ó el asignamiento de un procesador en ambientes de multiprocesamiento. 1.1. Palabras reservadas El lenguaje C++ Concurrente incluye las siguientes palabras reservadas a la lista del lenguaje C++: process nullprocess select when 1.2. Especificación de procesos La especificación de una clase de procesos establece su estructura interna y su comportamiento mediante la declaración de sus componentes variables o funciones. Un proceso es un verdadero programa secuencial que hereda de la clase Process, la capacidad de representar el flujo de control que posee. Cada proceso posee una pila propia para guardar el estado de su ejecución incluyendo el contenido de los registros del procesador y el área de almacenamiento de las variables locales, entre otros. La especificación de cualquier clase de proceso debe incluir al menos un constructor. La especificación de un proceso introduce un tipo definido por el programador que es consistente con los tipos provistos por el lenguaje (p.ej. los tipos básicos). Por ejemplo, la clase especial de procesos Writer, se especifica así: class Writer: Process{ int c; public: Writer(char*, int); }; Esta especificación permite declarar variables de tipo Writer, así como apuntadores, arreglos y referencias. 1.3. Definición de procesos La definición de un proceso la forma el cuerpo de la función constructora. El comportamiento, generalmente no determinístico del proceso, lo

Page 7: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

7

establece el texto de esta función. Diferentes funciones constructoras indicarán diferentes comportamientos de los procesos de la misma clase. Un proceso inicia su ejecución con el primer estatuto del bloque y termina luego de ejecutar el último estatuto que aparece en él. En otras palabras, la duración del proceso es la misma que la del bloque que lo define. El comportamiento de la clase de procesos Writer se puede formular de la siguiente forma: Writer::Writer(char* s, int n){

cout << "Proceso " << s << " inicia su actividad\n";

for(c = 0; c < n; c++){

cout << s << c;

suspend(); // Cede el control a otro proceso

}

cout << "Proceso " << s << " termina su actividad\n";

}

Aunque las reglas de alcance de los identificadores del C++ se conservan en el C++ Concurrente, las reglas de duración se han modificado para las variables locales declaradas en las funciones constructoras: 1. Las variables con atributo static se caracterizan por: (a) retener su valor, y (b) ser compartida por los procesos creados mediante la misma función constructora. 2. Las variables con atributo auto y los parámetros de la función constructora tienen la misma duración del proceso. Esta modificación en la regla de duración permite al proceso conservar el estado de su procesamiento. El programador debe cuidar de que no ocurra ningún cambio de contexto antes de que el proceso haya iniciado sus variables de estado. 1.4. Especificación del tamaño de la pila Un proceso de cualquier clase cuando se construye tiene un tamaño de pila estándar. Sin embargo, este tamaño puede cambiarlo el programador, si así lo desea, en la definición del constructor de su clase, invocando explícitamente al constructor de la clase base Process. Por ejemplo, si la clase Writer, no necesita demasiado espacio en la pila para su operación, entonces puede reducirse el tamaño de la pila indicando esto en el constructor de la clase base Process:

Page 8: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

8

Writer::Writer(char* s, int n): Process(128){

cout << "Proceso " << s << " inicia su actividad\n";

...

cout << "Proceso " << s << " termina su actividad\n";

}

que asigna a todos los procesos de esta clase creados con este constructor, un tamaño de pila de 128 palabras. Observe que la diferencia con el anterior constructor de Writer es la presencia de la expresión :Process(128) en el encabezado de la función. En el C++ Concurrente, es posible que un proceso no requiera de su pila. Esto ocurre cuando el proceso es un objeto en el sentido usual que se maneja en el lenguaje C++, es decir, su comportamiento se reduce a iniciar las variables de estado del proceso y no realiza ningún actividad posterior. Para especificarlo, se debe indicar que el proceso tiene una pila de tamaño cero. 1.5. Especificación de prioridad La política de administación de procesos se base en las siguientes reglas: 1. Un proceso de mayor prioridad se selecciona en preferencia a otro de menor prioridad. 2. A ningún proceso se le niega indefinidamente la ejecución con otros procesos de la misma prioridad. La prioridad es un número entero en el rango de 0 a 127, siendo cero la prioridad más baja que corresponde a los procesos de la clase Null que se analizará más adelante. La prioridad inicial de un proceso se puede especificar mediante el constructor de Process. Por ejemplo, la definición Writer::Writer(char* s, int n): Process(128, 100){

cout << "Proceso " << s << "inicia su actividad\n";

...

cout << "Proceso " << s << "termina su actividad\n";

}

establece que todos los procesos de la clase Writer tienen una prioridad inicial de 100. Por otra parte, durante la ejecución del proceso se puede

Page 9: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

9

fijar una nueva prioridad mediante la función setprio() y obtener la prioridad actual mediante getprio(). 1.6. Declaración de procesos Un proceso es una instancia de alguna clase derivada de Process. En un programa pueden coexistir diferentes instancias de la misma clase derivada. Siempre que no exista confusión, les llamaremos simplemente procesos a dichas instancias. Por ejemplo, los procesos a, b, y c, son instancias de Writer: main(void){

Writer a("Escritor A:", 4), b("Escritor B:", 2);

Writer c("Escritor C:",3);

Null os;

}

Cuando se declara una variable de cierta clase de procesos, se crea automáticamente un proceso aunque esto no significa que su actividad comienze de inmediato. El inicio de la actividad concurrente tiene lugar con la declaración de un proceso de la clase Null; sin ella no tendrá lugar tal actividad. Se considera que todo proceso declarado antes de la declaración de una instancia de Null, es suceptible de ponerse en ejecución. En adelante, si algún proceso A en ejecución declara a otro proceso B, entonces B está listo para iniciar su actividad en cualquier momento. Debemos aclarar que todo proceso, como la variable a de la clase Writer, posee una sección que proviene de la clase base Process. A ésta sección se le llama descriptor del proceso porque contiene información tan importante como su prioridad y la dirección de su pila, por mencionar algunas. 1.7. Construcción y destrucción de procesos Como se vió en el apartado anterior, un proceso se crea cuando se declara una variable mediante una invocación implícita a su constructor (creación estática), o durante la ejecución de un programa mediante una invocación explítica a su constructor, usando el operador new (creación dinámica). Un proceso se destruye cuando el control alcanza el final del bloque que lo define o por la aplicación del operador delete. Por ejemplo, el programa

Page 10: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

10

anterior puede reescribirse de la siguiente forma usando los operadores new y delete: main(void){

Writer *a, *b, *c;

a = new Writer("Escritor A:", 4);

b = new Writer("Escritor B:", 2);

c = new Writer("Escritor C:", 3);

delete new Null();

delete c;

delete b;

delete a;

}

Observe que aún cuando un proceso se haya destruido por haber alcanzado el final del bloque que lo define, su descriptor se destruirá hasta que se destruya el bloque que lo contiene o por la aplicación de delete. 1.8. Funciones componentes de procesos La clase Writer pertenece a una categoría muy grande de procesos que se distinguen por no ofrecer ningún servicio: se caracterizan por una intensa actividad que demanda servicios de otros procesos. Por otra parte, los procesos administradores de recursos ofrecen servicios que a su vez deben controlar. Los servicios que ofrece un proceso corresponden a las funciones componentes públicas de la clase (pero no las constructoras o destructoras) y pueden ser de cualquier tipo y tener cualquier número y tipo de parámetros. Los servicios ofrecen un medio para establecer la comunicación síncrona bidireccional entre dos procesos, ya que tiene lugar la transferencia de la información una vez lograda la sincronización. El solicitante de un servicio hace una llamada a la función pública correspondiente del prestador del servicio. Cuando el prestador del servicio se encuentra en condiciones de aceptarlo, lo hace de inmediato. En ese momento, ocurre la transferencia de la información a través de los parámetros de la función. El solicitante envía los datos necesarios usando el paso de parámetros por valor, y obtiene los resultados usando el paso de parámetos por referencia. Una vez otorgado el servicio, tanto el solicitante como el prestador pueden continuar de inmediato sus actividades. Sin embargo, si el prestador no se enconcontrara en condiciones de aceptar la solicitud, el solicitante debe esperar a que el prestador pueda aceptar la solicitud. Cuando

Page 11: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

11

eventualmente el prestador del servicio la acepta, comienza la atención a la solicitud como se ha descrito. Por ejemplo, la clase Buffer, define una cola circular usada como recipiente de mensajes. En el servicio put(int) que ofrece esta clase, el paso de parámetros se hace por valor ya que los datos son enviados al recipiente. En cambio, el servicio get(int&) usa el paso de parámetros por referencia ya que recibe los datos del recipiente. class Buffer: Process{

...

public:

Buffer(void);

void put(int);

void get(int&);

};

Puesto que los servicios que ofrece un proceso no pueden comenzar hasta que no se hayan iniciado las variables de estado, el proceso no debe ceder el control de la ejecución hasta después de la iniciación. 1.9. Apuntador al proceso en ejecución El proceso en ejecución se determina através de la variable global process, declarada en el archivo ccpp.h, cuyo tipo es apuntador a la clase Process. Esta variable es extremadamente útil en la depuración de programas y en los programas que emplen corrutinas. 1.10. El proceso nulo El proceso nulo es un proceso predefinido cuyo propósito es el de simplificar el mecanismo de inicio y fin de la actividad concurrente, así como el algoritmo de asignación del procesador. El proceso nulo posee la menor prioridad de todos los procesos. En este trabajo, el proceso nulo representa al sistema operativo que se encuentra suspendido en tanto dura el programa. Ocasionalmente, el proceso nulo se reactiva para responder a las solicitudes formuladas por otros procesos debido a que es el administrador de recursos tan importantes como la memoria.

Page 12: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

12

La responsabilidad del proceso nulo de satisfacer las necesidades de recursos de los procesos es una consecuencia del modelo de programación elegido en el C++ Concurrente. La consistencia que se obtiene al manejar de esta forma los recursos, deriva en la habilidad excepcional de usar el mismo administrador de memoria que provee el sistema operativo. Lo anterior ofrece la siguientes ventajas: 1. Reduce el tamaño de los programas porque no tiene que incluir el código del administrador de memoria. 2. Delega la responsabilidad del correcto uso de la memoria al compilador y al sistema operativo. 3. Los programas de aplicación pueden usar las bibliotecas de funciones provistas por el compilador. 1.11. Apuntador al proceso nulo El proceso nulo se determina mediante la variable global nullprocess que es un apuntador a la clase Null derivada de Process y declarada en el archivo ccpp.h. 1.12. Procesos y corrutinas En el lenguaje C++ Concurrente, la única diferencia entre procesos y corrutinas radica en el modo de como se transfiere el control ya que comparten la misma representación. Una corrutina es, al igual que un procedimiento, una unidad que agrupa y oculta, bajo el nombre de la corrutina, una serie de órdenes para realizar cierta actividad. Sin embargo, a diferencia del procedimiento, no existe una relación jerárquica entre llamador y llamado ya que el registro de activación de ambos tiene una duración mayor a la duración de la llamada. En este sentido (la duración de su ambiente) una corrutina es más parecida a un proceso. La diferencia radica, escencialmente, en la forma de como se trasfiere del control. La corrutina transfiere el control directamente a otra corrutina y, por esta razón, debe identificarla, por ejemplo, através de un apuntador. Un proceso, en cambio, cede el control y es el administrador del núcleo de concurrencia el que determina el siguiente proceso a ejecutarse. Un proceso cede el control indirectamente a algún otro através de la función suspend() del administrador de procesos. Una corrutina cede el

Page 13: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

13

control directamente a otro proceso mediante la función transfer() del administrador de procesos. Para ilustrar el uso de las corrutinas en el C++ Concurrente, hagamos una nueva versión del programa de los tres escritores. #include <stream.h>

#include "ccpp.h"

class Writer: Process{

int c;

public:

Writer(char*, int, Process*&);

};

En este ejemplo, el proceso a quien se debe transferir el control se pasa como una referencia a un apuntador. Esto permite construir programas con topología dinámica ya que el control se puede asignar al proceso indicado por el apuntador. Writer::Writer(char* s, int n, Process*& p){

cout << "Proceso " << s << " inicia su actividad\n";

for(c = 0; c < n; c++){

cout << s << c;

transfer(p); // Cede el control al proceso apuntado por p

}

cout << "Proceso " << s << " termina su actividad\n";

}

main(void){

Writer *a, *b, *c;

a = new Writer("Escritor A:", 4, b);

b = new Writer("Escritor B:", 2, c);

c = new Writer("Escritor C:", 5, a);

delete new Null();

delete c;

delete b;

delete a;

}

Este programa crea tres procesos que se usarán como corrutinas. Cada uno de ellos conoce la forma de referirse a otro mediante el parámetro p, activándose en forma circular mediante la operación transfer(p). Observe que las corrutinas tienen diferente duración siendo b la que dura menos.

Page 14: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

14

Eventualmente, cuando b haya terminado su ejecución, la operación de transferencia del control que realiza a ya no tendrá efecto. 1.13. Estatuto select Las regiones protegidas con guardias permiten a un proceso seleccionar una serie de acciones de entre varias conforme al estado interno del proceso. Si no es posible elegir alguna de ellas, se pospone la elección hasta que alguna se verifique. El estatuto select contiene los mecanismos necesarias para suspender la ejecución del solicitante cuando fallen todas las condiciones, y para reactivarlo más tarde a que intente de nuevo. Eventualmente, cuando se verifica alguna condición, el control ejecuta la serie de acciones que acompañan a la condición y abandona el estatuto. Por otra parte, cuando es posible elegir más de una alternativa, la elección se hace al azar. Esta incertidumbre garantiza la capacidad de modelar el no determinismo presente en las aplicaciones reales. En el lenguaje C++ Concurrente, las regiones protegidas sólo pueden emplearse en la definición de las funciones componentes de los procesos. Cuando el estatuto select aparece en la función constructora, el propio proceso es el único que puede suspenderse con este estatuto. Por otra parte, cuando aparece en los servicios, los procesos que lo soliciten son los que se pueden suspender. El estatuto select que tiene la forma: select clausula-when

en donde clausula-when es when(condición) alternativa

o {

when(condición-1) alternativa-1;

...

when(condición-n) alternativa-n;

}.

La semántica de las regiones protegidas se rige de acuerdo con las siguientes reglas.

Page 15: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

15

a. Un proceso A que ejecute el estatuto select puede entrar al bloque siempre que ningún otro proceso se encuentre dentro de él. b. Cuando algún otro proceso B se encuentra dentro del bloque, el proceso A se ve obligado a posponer su entrada. c. Cuando el proceso B abandona el bloque, se le permite su entrada a A. d. Si el proceso B prueba la condición que se encuentra en alguna de las cláusulas when y ésta se verifica, B puede entrar al bloque que le sigue. Si más de una se verifica, se elige una al azar y el proceso entra al bloque correspondiente. e. Por otra parte, si no se verifica ninguna condición, el proceso B se ve forzado a esperar suspendiendo de inmediato su actividad, permitiendo el acceso al proceso que lo desee. f. Eventualmente, la espera del proceso B termina, dando lugar a que se aplique de nuevo alguno de los casos a al f. g. Cuando existe más de un proceso que desea entrar a la región protegida, la elección del proceso se hace al azar. h. Ningún proceso A puede interrumpir la ejecución de otro proceso B hasta que B no haya fallado al probar las condiciones, o bien, que ya haya abandonado el estatuto select. Los servicios que ofrecen los procesos se consideran como operaciones indivisibles. En otras palabras, si el proceso A define dos operaciones P() y Q(), la primera de las cuales es solicitada por el proceso B, entonces A no puede ejecutar Q() hasta que B no haya terminado de ejecutar P() o se haya bloqueado. 2. CAMBIO DE CONTEXTO En ambientes con un solo procesador, la ejecución de los procesos se hace por el asignamiento del procesador en diferentes momentos. A esto se le llama seudoparalelismo. En ambientes con múltiples procesadores, los procesos pueden correr en un procesador propio. La mayoría de las implantaciones de núcleos de concurrencia contemplan algún administrador de procesos cuyas funciones subyacen en las construcciones del lenguaje.

Page 16: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

16

Durante la ejecución de un programa concurrente, algún evento puede causar que el proceso pierda el asignamiento del procesador. En este caso, se dice que el proceso se encuentra suspendido. Cuando esto ocurre, el proceso debe volver a recuperar el control en el misma lugar. Un proceso suspendido debe conservar el ambiente de su procesamiento de modo que pueda reanudar su actividad sin problemas. El ambiente se caracteriza por los contenidos de los registros del procesador, los contenidos del área de memoria propia, los descriptores de archivos y los puertos, entre otros. La implantación del núcleo de concurrencia debe asegurar la integridad del ambiente de los procesos suspendidos. Para que esto sea posible, un proceso debe poseer una pila propia en donde se conserve su estado, y de donde pueda recuperarlo más tarde. En el descriptor de proceso se conservan los apuntadores a la región de memoria en donde se encuentra su pila, así como cierta información adicional para la administración del proceso. El cambio de contexto consiste en las acciones necesarias para suspender el proceso activo, y para reactivar otro que esté en condiciones de reanudar su ejecución. Las acciones son: 1. Guardar el ambiente de la ejecución del proceso activo en su pila. 2. Guardar los apuntadores a la pila del proceso en su desciptor. 3. Elegir otro proceso que pueda reanudar su ejecución. 4. Restaurar los apuntadores a la pila para el proceso elegido. 5. Finalmente, restaurar el ambiente de la ejecución del proceso elegido. Como se ha dicho, el cambio de contexto se realiza cuando un proceso se ve forzado a suspender su actividad en la espera de condiciones favorables que le permitan, por ejemplo, adquirir recursos. Los mecanismos de sincronización y comunicación, implícitos en las construcciones que incorpora el lenguaje C++ Concurrente, usan funciones que realizan el cambio de contexto. La primera y la última de las acciones las realiza el prólogo y el epílogo de la función que realiza el cambio de contexto. La segunda y la cuarta la realiza la función usando el apuntador al proceso en ejecución process. Precisamente, el propósito de las funciones de cambio de contexto es obtener el nuevo proceso que reciba el control, guardando en process la dirección de su descriptor.

Page 17: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

17

Por lo general, en un cambio de contexto existe más de un proceso que puede recibir el procesador por lo que se forman en la cola predefinida por el sistema readyQueue. Para resolver los conflictos que pueden surgir, se debe seguir una política de asignamiento. La política más común es la de formarse de nuevo para recibir el procesador (round-robin). Como su nombre lo sugiere, un proceso que ha tenido el control puede volver a tenerlo siempre que se forme de nuevo al final de la cola de candidatos. Existen algunas variantes de este esquema basados en la prioridades. La prioridad establece una preferencia en la ejecución de un proceso sobre otro; en este caso, se dice que el primero tienen mayor prioridad que el segundo. Cuando dos procesos tienen igual prioridad, se da preferencia al más antiguo. 2.1. El cambio de contexto en los procesos Para ilustrar lo anterior, vamos a analizar el programa de los tres escritores que ceden el control luego de cada escritura. // Definiciones y declaraciones importantes

#include <stream.h>

#include "ccpp.h"

// Especificación del proceso

class Writer: Process{

int c;

public:

Writer(char*, int);

};

// Definición del proceso

Writer::Writer(char* s, int n){

cout << "Escritor " << s << " inicia su actividad\n";

for(c = 0; c < n; c++){

cout << s << c;

suspend(); // Cede el control a otro proceso

}

cout << "Escritor " << s << " termina su actividad\n";

}

// Declaración y ejecución

main(void){

Writer a("A:", 4), b("B:", 2), c("C:",5);

Null os;

}

Page 18: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

18

Este programa declara una clase específica de procesos escritores Writer. Cada escritor posee una variable privada c de tipo entero, y una función pública Writer() que es el constuctor de la clase. El constructor posee un parámetro usado para distinguir a las diferentes instancias de Writer y otro para indicar el número de escrituras que realizará cada proceso. El constructor establece el comportamiento del proceso: escribe el parámetro que lo identifica así como el número que lleva la cuenta de las iteraciones y luego, suspende su ejecución con una llamada a suspend(). La función main() crea tres procesos escritores a, b y c, y al proceso nulo os. Los tres procesos a, b y c tienen el mismo tamaño de su pila y la misma prioridad. La pila del proceso nulo os es la pila original del sistema operativo, asegurando que se conserve "intacto" mientras se encuentre suspendido. La prioridad de os es la menor posible, garantizando que se reactive hasta que no haya otros procesos que puedan recibir el procesador como ocurre al final del programa. En este momento, los cuatro procesos se encuentran formados en la cola readyQueue por ser los candidatos a recibir el control. El programa comienza con la función main(), ejecutando secuencialmente sus estatutos. Como se dijo antes, en el lenguaje C++, cuando se declara una instancia de una clase se ejecuta en ese momento el constructor de su clase. Si se trata de una clase derivada, se debe ejecutar antes el constructor de la clase base. Puesto que la clase base de Writer es Process, se debe ejecutar primero Process::Process(), creando así el descriptor del proceso para que guarde la información relevante del proceso. Al terminar de ejecutarse, el control regresa a Writer::Writer(). Sin embargo, antes de comenzar a ejecutar la primera instrucción de esta función (el estatuto for en el ejemplo), el control se transfiere de nuevo a main() para que pueda crear otro proceso en forma similar. Este "extraño" comportamiento es el que se necesita para dar la ilusión de concurrencia. De esta forma se crean los descriptores de los tres procesos a, b y c, los cuales aún no comienzan propiamente su ejecución, encontrándose suspendidos y listos para recibir el control. Finalmente, cuando el control alcanza al proceso nulo os, al igual que los anteriores se crea su descriptor, pero a diferencia de ellos es el único tipo de proceso que ejecuta a su constructor Null::Null(). Esta función es la responsable de ceder el control a alguno de los procesos previamente creados mediante la orden suspend(), lo que da inicio a la actividad concurrente. Los tres procesos escritores y el proceso nulo pueden recibir el control; sin embargo, debido a que los escritores tienen mayor prioridad, serán los escritores los únicos que se manifiesten. La elección del escritor a

Page 19: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

19

ejecutarse se hace al azar debido a que tienen la misma prioridad. Por ejemplo, si b es el que gana el control, entonces entra en el ciclo, escribe su mensaje y se suspende. Entonces, el proceso b se forma en la lista de elegibles y es, en este momento, cuando se elige al siguiente. Esto da como resultado que a, c, e inclusive, b puedan obtener el control. El escritor termina su actividad cuando alcanza el final del Writer::Writer(), invocando de inmediato a una función que se encarga de liberar el espacio de la pila usada por el proceso y de evitar que compita más por el procesador. Luego, cede el control a otro proceso elegible. De esta forma, el proceso se destruye como entidad activa aún cuando queda su descriptor como entidad pasiva. Cuando todos los escritores han sido destruidos de esta manera, solo queda el proceso nulo como el único candidato para adquirir el control. Cuando lo hace, el control aparece en la instrucción siguiente a suspend(), en el código del constructor Null::Null(), justo donde se había quedado al iniciar la concurrencia y luego, el control regresa a main(). Finalmente, se ejecutan los destructores de los descriptores y el programa termina. Aunque aquí solo se ha discutido la creación estática de procesos, la creación dinámica es similar teniendo en cuenta que el programador debe manejar explícitamente a los constructores y destructores de procesos. La técnica descrita en éste apartado fue desarrollada por el autor y usada en la implantación de su trabajo de tesis de maestría, el lenguaje PL/M-86 Concurrente [8]. La técnica sugiere un esquema general y a la vez flexible, para extender un lenguaje estructurado por bloques. 2.2. El cambio de contexto en las corrutinas En el C++ Concurrente, la única diferencia entre proceso y corrutina es el modo de ceder el control. Otra forma de decirlo, es que los procesos pueden ceder el control al estilo de las corrutinas. De hecho, desde el punto de vista del modelo de proceso distribuido, las corrutinas son casos particulares de los procesos. La llamada transfer(newprocess) transfiere directamente el control al proceso apuntado por newprocess, quien a su vez puede ceder el control a otro directamente (como corrutina) o indirectamente (como proceso). En tanto, el proceso que originalmente cedió el control se quedará esperando. Cuando lo reciba, su ejecución se reanudará en el estatuto que sigue a la

Page 20: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

20

llamada de transfer(newprocess). Si newprocess ya fué destruido, transfer(newprocess) no tiene ningún efecto. 3. EJEMPLOS A continuación se presentan algunos problemas de concurrencia y su solución usando el lenguaje C++ Concurrente. La mayoría de ellos fueron tomados directamente del artículo de Brinch Hansen [3]. Esperamos que el lector interesado los estudie y los compare con las soluciones escritas en otros lenguajes concurrentes. Los programas que aquí aparecen fueron probados en el prototipo desarrollado en el lenguaje C++ de Borland. 3.1. Semáforos Un semáforo consiste de un contador cntr, de una cola de procesos queue y de dos operaciones que lo accesan wait() y signal(). Cuando el contador es un número negativo, el proceso que emite la operación wait() se bloquea hasta que el contador sea cero o un número positivo. La operación signal() incrementa siempre al contador rehabilitando el proceso suspendido por wait(). #include "ccpp.h"

// Especificación de la clase

class Semaphore: Process{

Queue queue;

int cntr;

public:

Semaphore(int);

void wait(void);

void signal(void);

};

Semaphore::Semaphore(int n): Process(0){

if(n >= 0) cntr = n;

}

void Semaphore::wait(void){

if(--cntr < 0) suspend(queue, readyQueue);

}

void Semaphore::signal(void){

if(cntr++ < 0) readyQueue.add(queue.sub());

}

Page 21: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

21

La llamada suspend(queue, readyQueue) que aparece en wait() establece que el proceso en ejecución será formado en queue y que el siguiente será obtenido de la cola de candidatos readyQueue. Por otra parte, en la llamada readyQueue.add (queue.sub()) que aparece en signal(), extrae al proceso que primero haya llegado a queue, y después lo hace elegible para recibir el control insertándolo en readyQueue. Se puede formular una versión equivalente usando regiones protegidas como lo sugiere Brinch Hansen. Recuerde que las regiones protegidas sólo pueden usarse en la definición de las funciones componentes de un proceso de modo que en esta versión, la clase Semaphore debe especificarse como un proceso, aunque jamás volverá a competir por el procesador después de realizar la iniciación. De hecho, el proceso se destruye lo cuál no tiene ningún efecto sobre los procesos que usan las operaciones, debido a que la actividad del proceso servidor no incluye la administración del semáforo. #include "ccpp.h"

class Semaphore: Process{

int cntr;

public:

Semaphore(int init);

void wait(void);

void signal(void);

};

Semaphore::Semaphore(int init): Process(0){

if(init >= 0) cntr = init;

}

void Semaphore::wait(void){

select when(cntr > 0) cntr--;

}

void Semaphore::signal(void){

cntr++;

}

Los procesos que usen estas operaciones deben hacerlo así: {

Semaphore s(1);

s.wait();

... // Región crítica

s.signal();

}

Page 22: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

22

lo que ilustra una solución al problema de la exclusión mutua usando semáforos. El proceso semáforo es un caso típico de un prestador de servicios: su actividad se reduce a establecer los valores iniciales de las variables. En adelante, solamente atiende las solicitudes wait() y signal() de otros procesos. 3.2. Recipiente de mensajes Un proceso recipiente de mensajes almacena una secuencia de números enteros transmitidos entre procesos por medio de las operaciones send() y receive(). #include "ccpp.h"

class Buffer: Process{

int sz, cn, hd, tl, *st;

void get(int&);

void put(int);

public:

Buffer(int);

void send(int);

void receive(int&);

};

Buffer::Buffer(int size): Process(0){

cn = hd = tl = 0; st = new int[sz = size];

}

void Buffer::put(int& c){

st[tl] = c; tl = (tl + 1) % N; cn++;

}

void Buffer::send(int c){

select when(cn < sz) put(c);

}

void Buffer::get(int c){

c = st[hd]; hd = (hd + 1) % N; cn--;

}

void Buffer::receive(int& c){

select when(cn > 0) get(c);

}

Las operaciones sobre el recipiente se pueden hacer así: {

Buffer b(100); int x = 7;

b.send(x); // Envía el contenido de x

Page 23: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

23

...

b.receive(x); // Recibe un mensaje en x

}

Al igual que los semáforos, después de iniciar sus variables el Buffer no requiere más el procesador, porque solamente define las estructuras de datos y las operaciones significativas sobre ellas. 3.3. Administrador de tareas Existe una gran variedad de problemas de administración de recursos que se pueden resolver mediante regiones protegidas. Un recurso es por naturaleza "un cuello de botella" cuya rendimiento aceptable depende de que sean pocos los procesos que lo soliciten por mucho tiempo, o bien, que sean muchos procesos pero que lo usen por poco tiempo. Un administrador de recursos que sigue la política el más corto primero, (shortest job next) asigna un recurso a un número limitado de N procesos de usuarios. La solicitud del recurso request() proporciona la identidad del proceso solicitante y el tiempo de servicio que requiere. La liberación del recurso release() indica que el recurso se encuentra de nuevo disponible. El administrador usa las siguientes variables: una lista de identificadores representada por un conjunto de bits queue, un arreglo de enteros para guardar el tiempo que cada proceso requiere rank, el índice del usuario actual (si lo hay) user, y el índice del usuario siguiente next. La especificación de la clase Set y de Scheduler se dan a continuación: #include <stream.h>

#include "ccpp.h"

#define N 16

#define NIL -1

#define MININT 32767

class Set{

int bits, size;

public:

Set(void){bits = size = 0;}

void include(int){if(0 <= e && e < N){bits |= 1<<e; size++;}}

void exclude(int){if(0 <= e && e < N){bits &= ~(1<<e); size--;}}

int contain(int){if(0 <= e && e < N) return bits & (1<<e);

int card(void){return size;}

Page 24: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

24

};

class Scheduler: Process{

Set queue;

int rank[N];

int user, next;

public:

Scheduler(void);

void request(int, int);

void release(void);

};

Una vez iniciadas las variables, el administrador espera hasta que una de dos situaciones tiene lugar: 1. Un proceso entra o sale de la lista queue: el administrador examinará la lista y seleccionará al siguiente usuario si es que lo hay. Sin embargo, esto no significa que el recurso se le asigne. 2. El recurso no está siendo usado y el siguiente usuario ya había sido seleccionado: el administrador asigna entonces el recurso al proceso seleccionado y lo remueve de la lista de espera. Scheduler::Scheduler(void){

user = next = NIL;

for(;;)

select{

when(queue.card() > 0 && next == NIL){

int i, k, min = MININT;

for(i=0, k=0; i < N && k < queue.card(); i++)

if(queue.contain(i) && rank[i] <= min){

next = i; min = rank[i]; k++;

}

}

when(user == NIL && next != NIL){

user = next; queue.exclude(user);

}

}

}

void Scheduler::release(void){

user = NIL;

}

void Scheduler::request(int who, int size){

Page 25: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

25

queue.include(who);

rank[who] = size;

select when(user == who) next = NIL;

}

Los procesos de usuario se identifican mediante un número único entre 0 y N-1. Su comportamiento consiste en solicitar el recurso administrado por sjn por un tiempo arbitrario, y más tarde, en liberarlo. class User: Process{

public:

User(Scheduler&,int);

};

User::User(Scheduler& sjn, int id){

int r;

for(int i = 0; i < 100; i++){

r = random(5)+1;

cout << id << "solicita el recurso por " << r << "secs.\n";

sjn.request(id, r);

cout << id << "usando el recurso por " << r << "secs.\n";

delay(r);

sjn.release();

cout << id << "usó el recurso\n";

}

}

El programa principal consiste de las declaraciones de los procesos incluida la del proceso nulo. main(){

Scheduler s;

User a(s,0), b(s,1), c(s,2), d(s,3);

Null os;

}

La administración de cada proceso se maneja en dos niveles de abstracción: al nivel de las operaciones request() y release(), y al nivel de las regiones protegidas con select. La evaluación periódica de la condición de sincronización puede ser una serie sobrecarga para el rendimiento del sistema. Sin embargo, es aceptable cuando se hace la distribución del procesamiento y un solo procesador está dedicado a ello.

Page 26: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

26

3.4. Lectores y escritores Dos clases de procesos, llamados lectores y escritores, comparten un recurso. Los lectores pueden usar el recurso simultaneamente, pero cada escritor debe tener acceso exclusivo a él. Los lectores y escritores se comportan como sigue: Una variable s define el estado actual del recurso como uno de los siguientes: s = 0 1 escritor usa el recurso s = 1 0 procesos usan el recurso s = 2 1 lector usa el recurso s = 3 2 lectores usan el recurso ... El anterior esquema da lugar a la siguiente solución [12]. El proceso Resource regula el paso de los lectores Reader y de los escritores Writer mediante las operaciones startread, endread, startwrite, endwrite, respectivamente. #include <stream.h>

#include "ccpp.h"

#define TIME 500

class Resource: Process{

int s;

public:

Resource(void);

void startread(void);

void endread(void);

void startwrite(void);

void endwrite(void);

};

class Reader: Process{

public:

Reader(int, Resource&);

};

class Writer: Process{

public:

Writer(int, Resource&);

};

Page 27: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

27

Las definiciones de los procesos y sus operaciones son: Resource::Resource(void): Process(0){

s = 1;

}

void Resource::startread(void){

select when(s >= 1) s++;

}

void Resource::endread(void){

if(s > 1) s--;

}

void Resource::startwrite(void){

select when(s == 1) s = 0;

}

void Resource::endwrite(void){

if(s == 0) s = 1;

}

Reader::Reader(char* id, Resource& book){

for(int i = 0; i < 10; i++){

cout << "Lector %s solicita recurso " << id;

book.startread();

cout << "Lector %s usando el recurso..." << id;

delay(TIME);

book.endread();

cout << "Lector %s libera el recurso " << id;

}

}

Writer::Writer(int id, Resource& book){

for(int i = 0; i < 10; i++){

cout << "Escritor %s solicita recurso " << id;

book.startwrite();

cout << "Escritor %s usando el recurso..." << id;

delay(TIME);

book.endwrite();

cout << "Escritor %s libera el recurso " << id;

}

}

main(){

Resource book;

for(int i = 0; i < 10; i++){

new Reader(i, book);

new Writer(i, book);

Page 28: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

28

}

new Null();

}

3.5. Los filósofos Cinco filósofos alternan sus actividades de pensar y comer. Cuando un filósofo tiene hambre, se dirige a una mesa redonda y toma los dos cubiertos más cercanos al plato y comienza a comer. Sin embargo, solamente hay cinco cubiertos en la mesa, asi que un filósofo sólo puede comer cuando ninguno de sus vecinos inmediatos lo está haciendo. Cuano un filósofo termina de comer, pone los dos cubiertos de nuevo en la mesa y la abandona. La especificación de las clases Table y Philosopher es como sigue: #include <stream.h>

#include "ccpp.h"

class Table: Process{

Set eating;

public:

Table(void);

void join(int);

void leave(int);

};

class Philosopher: Process{

public:

Philosopher(int,Table&);

};

El proceso Table usa la clase conjunto Set definida en el ejemplo 3. El conjunto eating registra a los filósofos que se encuentran comiendo en la mesa. Por esta razón, la operación join() establece que es necesario esperar hasta que los dos filósofos que lo rodean hayan dejado de comer para que el pueda empezar. Cuando el filósofo abandona la mesa, se debe excluir del conjunto eating. Table::Table(void): Process(0){ // Actividad totalmente nula

}

void Table::join(int i){

select

when(!eating.contain((i+4) % 5)

&& !eating.contain((i+1) % 5))

Page 29: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

29

eating.include(i);

}

void Table::leave(int i){

eating.exclude(i);

}

El filósofo i aplica las operaciones table.join(i) y table.leave(), en ese orden, para sincronizarse con los otros filósofos. Philosopher::Philosopher(int id, Table& table){

for(i = 0; i < 10; i++){

cout << "Filósofo %d hambriento" << id;

table.join(id);

cout << "Filósofo %d comiendo...." << id;

delay(TIME);

table.leave(id);

cout << "Filósofo %d pensando" << id;

delay(TIME);

}

}

Los filósofos se pueden crear como un arreglo de procesos, o más exactamente, como un arreglo de apuntadores a procesos Philosopher. Observe, que este programa no necesita realmente conocer las direcciones de los filósofos por lo que podemos precindir de arreglo. main(){

Table table;

for(int i = 0; i < 5; i++)

new Philosopher(i, table);

new Null();

}

La solución que se presenta no previene que dos filósofos coman alternativamente, impidiendo que coma el filósofo que se encuentra en medio. 3.6. Ordenamiento de un arreglo Un arreglo de procesos puede ordenar un arreglo en un tiempo proporcional al tamaño de un arreglo de datos. Los datos se envían al primer proceso del arreglo quien conserva al menor de todos ellos y pasa

Page 30: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

30

el resto al segundo. Más tarde, el segundo proceso conserva el menor de los datos que ha recibido y envía el resto al siguiente, y así sucesivamente. Cuando se agotan los datos del arreglo, cada proceso mantendrá el dato que le corresponde de acuerdo con su orden natural: el primer proceso conserva el menor de los datos, el segundo conserva el dato que le sigue, etc. Para recuperar los datos ya ordenados, solicitamos los datos al primer proceso del arreglo quien responderá de inmediato con el dato que guarda. Más tarde, el segundo proceso envía el dato que guarda al primer proceso. Este procedimiento hace que los datos se muevan hacia los predecesores del arreglo hasta que finalmente se agotan los datos. #include <stream.h>

#include "ccpp.h"

class Sort: Process{

int here[2], length, rest, temp;

public:

Sort(int);

void put(int);

void get(int&);

};

Sort* sort[N];

El proceso Sort usa la variable here para guardar los datos que recibe, length para indicar el número de ellos, rest es el número de datos pasados a sus sucesores y temp contiene el dato que será pasado a su sucesor. Las operaciones put() y get() envían y reciben los datos hacia y del proceso, respectivamente. La variable global sort es el arreglo de N apuntadores a los procesos que realizarán el ordenamiento. class User: Process{

public:

User(void);

};

El proceso de usuario User envía y recibe los datos al primer proceso del arreglo con las llamadas sort[0]->put() y sort[0]->get(), respectivamente . User define el arreglo a de M enteros que serán ordenados. El número de elementos del arreglo M debe ser menor al número de procesos N. User::User(void){

int i, a[M]={6,2,7,9,3,5,8,1,0,4};

cout << "\n\nArreglo desordenado: ";

for(i = 0; i < M; i++)

Page 31: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

31

cout << a[i];

for(i = 0; i < M; i++)

sort[0]->put(a[i]);

for(i = 0; i < M; i++)

sort[0]->get(a[i]);

cout << "\nArreglo ordenado: ";

for(i = 0; i < M; i++)

cout << a[i];

};

Un proceso del arreglo se encuentra en equilibrio cuando guarda un dato nada más. Dicho equilibrio se ve alterado por el proceso predecesor cuando le envía un dato o cuando se lo solicita. Para recuperar el equilibrio, se debe tener en cuenta dos situaciones: 1. Si el proceso posee dos datos, mantendrá el menor de los dos y pasará el mayor a su sucesor. 2. Si el proceso no posee ningún dato pero su sucesor sí, entonces se lo solicita a él. El índice succ determina el proceso siguiente: Sort::Sort(int succ){

length = rest = 0;

for(;;)

select{

when(length == 2){

if(here[0] <= here[1])

temp = here[1];

else {

temp = here[0]; here[0] = here[1];

}

length--; sort[succ]->put(temp); rest++;

}

when(length == 0 && rest > 0){

sort[succ]->get(temp);

rest--; here[0] = temp; length++;

}

}

}

void Sort::put(int c){

Page 32: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

32

select when(length < 2){

here[length] = c;

length++;

}

}

void Sort::get(int& c){

select when(length == 1){

length--;

c = here[length];

}

}

Los procesos se crean mediante el operador new guardando sus direcciones en sort. main(){

for(int i = 0; i < N; i++)

sort[i] = new Sort(i+1);

new User();

new Null();

}

3.8. Construcción dinámica de procesos El lenguaje C++ concurrente permite construir procesos a tiempo de ejecución. A diferencia de los ejemplos anteriores, es posible que un proceso construya a otro (inclusive de otra clase) mediante las funciones new y delete. Para demostrarlo, presentamos el ejemplo clásico del programa factorial que crea nuevos procesos en forma recursiva. class Factorial: Process{

Factorial* factorial;

Semaphore* next;

public:

Factorial(long n, long& r, Semaphore* prev);

};

Factorial::Factorial(long n, long& r, Semaphore* prev){

if(n == 0){

r = 1;

prev->signal();

}

else {

next = new Semaphore(0);

Page 33: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

33

factorial = new Factorial(n - 1, r, next);

next->wait();

r = n * r;

prev->signal();

delete factorial;

delete next;

}

}

class Caller{

Factorial* factorial;

Semaphore* next;

public:

Caller(long n, long& r);

};

Caller::Caller(long n, long& r){

next = new Semaphore(0);

factorial = new Factorial(n, r, next);

next->wait();

delete factorial;

delete next;

}

main(){

long n = 10, r;

Caller factorial(n, r);

Null os;

cout << "Factorial(" << n << ")=" << r;

}

El proceso factorial de n, crea a un semáforo next, y a otro proceso factorial para n-1. El parámetro n se envía del factorial de n al factorial de n-1. El parámetro r lo recibe el factorial de n del factorial de n-1. El semáforo next, sincroniza a los procesos, de modo que el factorial de n espera a que el factorial de n-1 calcule el resultado. Cuando esto ocurre, el factorial de n reanuda su ejecución. El proceso Caller sólo se usa para iniciar las llamadas al Factorial. Los dos ejemplos anteriores demuestran algunas de las capacidades del C++ Concurrente: (1) la creación y destrucción dinámica de procesos, (2), la topología dinámica de procesos, y, (3) la comunicación síncrona bidireccional. 3.9. Trayectorias (Path expressions)

Page 34: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

34

Las trayectorias definen secuencias de operaciones similares a las expresiones regulares [Campbell, R.H. & Habermann, A.N. 1974]. Las trayectorias se pueden representar por procesos que definen las operaciones y de variables de estado que sincronizan su actividad estableciendo un orden en la ejecución de las operaciones. Secuencia. Supongamos que la operación P debe ejecutarse antes que la operación Q, como se muestra en la figura, -> P -> Q ->

en el lenguaje C++ Concurrente se puede obtener un esquema correspondiente, introduciendo estados (a, b y c) -> [s == a] P -> [s == b] Q -> [s == c]

y usando regiones críticas Path::P(){select when(s == a){...; s = b;}}

Path::Q(){select when(s == b){...; s = c;}}

Alternativas. El diagrama que sigue indica que tanto la operación P como la operación Q pueden ejecutar en un momento dado. |-> [s == a] P ->|

->| |-> [s == b]

|-> [s == a] Q ->|

que corresponde a Path::P(){select when(s == a){...; s = b;}}

Path::Q(){select when(s == a){...; s = b;}}

Repeticiones. El siguiente diagrama muestra la repetición de la operación P cero, una o más veces. |<- P [s == a] <-|

->|-> -> ->| -> [ s == a]

que corresponde a Path::P(){select when(s == a){...}}

Page 35: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

35

Las trayectorias pueden usarse para ambientes de programación visual que se basen en algún modelo de concurrencia para imponer un orden en su evaluación. El administrador de recursos presentado en el ejemplo 3, se puede representar por medio de trayectorias formadas por la secuencia de operaciones request() ... release() de cero, una o más ocasiones. El problema de los lectores y escritores ilustra el uso de variables de estado que permitan que algunas operaciones tengan lugar simultaneamente mientras otras son excluidas. Por ejemplo, varios lectores pueden operar simultaneamente excluyendo a los escritores, o bien, que un escritor puede excluir a los lectores.

Page 36: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

36

4. DIRECCIONES FUTURAS Este trabajo se puede extender en varias direcciones, sobre algunas de las cuales, ya se ha comenzado a trabajar. 4.1. Desarrollar un preprocesador externo para mejorar el rendimiento de los programas diseñados con la implantación actual. 4.2. Incorporar algunas construcciones que le den mayor poder y expresividad al lenguaje. En particular, dichas construcciones establecen una forma de resolver la asimetría que se presenta cuando se usan las regiones protegidas, en donde, el proceso dueño de la región es el único que impone las condiciones necesarias para satisfacer las solicitudes de servicio. 4.3. Incluir objetos remotos. Los objetos remotos son una abstracción que permite unificar diferentes conceptos de la programación: llamada a un procedimiento local, llamada a un procedimiento remoto. Este enfoque permite tratar en forma coherente, a varios conceptos: las comunicaciones, las bases de datos heterogeneas, las bases de datos distribuidas, etc. Estas direcciones son temas importantes de investigación actual y el plateamiento de la programación concurrente orientada a objetos puede usarse como marco de referenica para el desarrollo de sistemas simples y eficientes. 5. CONCLUSIONES El lenguaje C++ Concurrente se basa en el modelo de proceso distribuido de Brinch Hansen. La razón de haber elegido este modelo es la generalidad con la que trata diferentes conceptos de la programación concurrente y la orientada a objetos, así como la facilidad para implementarse. El concepto de proceso distribuido incluye un considerablemente amplio conjunto de conceptos como casos especiales lo que demuestra su enorme poder expresivo. El lenguaje C++ Concurrente encuentra aplicaciones en diferentes áreas: lenguajes de programación, programación concurrente, programación en tiempo real, programación orientada a objetos, análisis y diseño asistido por computadora, computación distribuida, control automático, sistemas

Page 37: El lenguaje C++ Concurrente - .:: · PDF file3 El contenido de este artículo es como sigue: la primera sección describe las extensiones del lenguaje C++ Concurrente, la segunda sección

37

operativos, protocolos de comunicación, redes de computadoras y simulación, entre otras. 6. REFERENCIAS

1. Andler, S. [1979] "Predicate path expressions". Conference record of the sixth annual ACM Symposium on principles of programming languages.

2. Brinch Hansen, P. [1973] "Operating system principles". Prentice Hall.

3. Brinch Hansen, P. [1978] "Distributed process: a concurrent programming concept". CACM, vol.21, no. 11.

4. Dijkstra, E. W. [1968] "Guarded commands, nondeterminacy and formal derivation of programs". CACM. vol.18, no. 8.

5. Ichibiah, [1983] "Reference manual for the Ada programming language". United States Departament of Defense.

6. Gehani, N. and Roome, W. [1989] "The Concurrent C". Silicon Press.

7. Hoare, C. A. R. [1978] "Communicating Sequential Process". CACM, vol.17, no.10 (October), pp.549-557.

8. Olmedo, O. [1987] "PL/M-86 Concurrente". Tesis de maestría. Sección de Computación. Departamento de Ingeniería Eléctrica. CINVESTAV IPN.

9. Peterson, J. & Silverschatz, A. [1981] "Operating systems concepts". Prentice Hall.

10. Stroustrup, B. [1986] "The C++ Programming Language". Adisson Wesley.

11. Dahl, O., Dijkstra, E., & Hoare, C.A.R., "Structured Programming". 12. Brinch Hansen, P. & Staunstrup, J. [1977] "Specification and

implementation of mutual exclusion". Comptr. Sci. Dept., U. of Southern California, Los Angeles.

13. Hansen,T.L., [1990] "The C++ answer book". Addison Wesley.