Universidad Autónoma Metropolitana Unidad Iztapalapa División de Ciencias Básicas e Ingenierı́a “Simulador DFS Distribuido de Eventos Discretos” Proyecto de Ingenierı́a Electrónica Grado: Licenciatura David Cortés Poza 99321198 Alejandro Martı́nez Ramı́rez 97319967 Asesor: Dr. Ricardo Marcelı́n Jiménez 3 de marzo de 2004 1 Índice I MARCO TEÓRICO 4 1. Prefacio 4 2. La simulación de eventos discretos 6 3. Métodos de sincronización en la simulación 6 4. Algoritmo DFS 7 5. Funcionamiento del simulador utilizado 9 II PROGRAMACIÓN DISTRIBUIDA 6. Herramientas de la simulación distribuida 6.1. MPI (Message-Passing Interface) . . . . . 6.1.1. INSTALACIÓN DE MPICH . . . . 6.1.2. Instalación de LAM / MPI . . . . . 6.2. Configuración final . . . . . . . . . . . . . 6.2.1. MPICH . . . . . . . . . . . . . . . 6.2.2. MPI/LAM . . . . . . . . . . . . . . . . . . . . . . . . . . 12 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 12 13 13 14 15 15 7. Compilación y ejecución de programas. 16 8. Comunicación básica con MPI 8.1. Funciones de MPI . . . . . . . 8.2. Envı́o y recepción de mensajes 8.3. Otras funciones de MPI . . . 8.4. Ejemplo de Broadcast . . . . 16 16 18 18 19 III . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PROCEDIMIENTO Y RESULTADOS . . . . . . . . . . . . . . . . . . . . . . . . 21 9. El algoritmo DFS distribuido 21 10.Adaptación del código existente 21 2 11.Resultados Finales 25 12.Observaciones y Conclusiones 33 13.Trabajo por hacer 34 IV 36 APÉNDICES 14.Código fuente del simulador DFS distribuido 14.1. Clase: DFS / Archivo dfs.cc . . . . . . . . . . 14.2. Clase: Simulation . . . . . . . . . . . . . . . . 14.2.1. Archivo simulation.h . . . . . . . . . . 14.2.2. Archivo simulation.cc . . . . . . . . . . 14.3. Clase: Model . . . . . . . . . . . . . . . . . . 14.3.1. Archivo model.h . . . . . . . . . . . . 14.3.2. Archivo model.cc . . . . . . . . . . . . 14.4. Clase: Event . . . . . . . . . . . . . . . . . . . 14.4.1. Archivo event.h . . . . . . . . . . . . . 14.4.2. Archivo event.cc . . . . . . . . . . . . 14.5. Clase: Process . . . . . . . . . . . . . . . . . . 14.5.1. Archivo process.h . . . . . . . . . . . . 14.5.2. Archivo process.cc . . . . . . . . . . . 14.6. Clase: Simulator . . . . . . . . . . . . . . . . . 14.6.1. Archivo simulator.h . . . . . . . . . . . 14.6.2. Archivo simulator.cc . . . . . . . . . . 14.7. Clase: Graph . . . . . . . . . . . . . . . . . . 14.7.1. Archivo graph.h . . . . . . . . . . . . . 14.7.2. Archivo graph.cc . . . . . . . . . . . . 14.8. Clase: MPIComm . . . . . . . . . . . . . . . . 14.8.1. Archivo mpicomm.h . . . . . . . . . . 14.8.2. Archivo mpicomm.cc . . . . . . . . . . 14.9. Clase: LinkList / Archivo linklist.cc . . . . . . 14.10.Clase: AVLTree / Archivo: avltree.cc . . . . . 15.Bibliografı́a . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 36 39 39 40 42 42 43 45 45 45 47 47 48 50 50 51 52 52 53 55 55 56 60 70 85 3 Parte I MARCO TEÓRICO 1. Prefacio La simulación por medio de las computadoras es la disciplina de diseño de un modelo de un sistema fı́sico real o teórico, ejecutando el modelo en una computadora y analizando la ejecución y los datos de salida. La simulación engloba el principio de “aprender haciendo”; se aprende acerca de un sistema construyendo un modelo de algún tipo y luego operando este modelo. El uso de la simulación es una actividad que es tan natural como un niño en un juego de rol. Los niños entienden el mundo que los rodea por medio de juguetes y disfraces que les permiten simular su interacción con otras personas, animales y objetos. Como adultos, se pierden algunos de estos comportamientos infantiles pero se recapturan en cierto modo por medio de la simulación por medio de las computadoras. Para entender la realidad y toda su complejidad, se deben construir objetos artificiales y asumir roles con ellos. Este tipo de simulación sirve para interactuar en ambientes sintéticos y mundos virtuales. El concepto de simulación está dividido en tres partes: diseño del modelo, ejecución del modelo y el análisis del modelo. Para simular un sistema fı́sico, primero se necesita crear un modelo (que en general es matemático) que represente ese o esos objetos fı́sicos. Estos modelos pueden ser de tipo aseverativo, funcional, restringido, espacial o multimodelo. Un multimodelo es un modelo que contiene múltiples modelos integrados y dependiendo del número de modelos, se proporciona un nivel de precisión para la simulación del sistema fı́sico. La tarea siguiente, una vez que el modelo ha sido desarrollado, es la ejecución del modelo en una computadora; esto es la creación de un programa de computadora que conforme avance el tiempo, el estado de las variables y de los eventos del modelo matemático vaya siendo actualizado. Existen varias maneras de “avanzar en el tiempo”. Se puede, por ejemplo, avanzar gracias a una agenda de eventos o se pueden emplear pequeños incrementos de tiempo (time slicing). Se puede ejecutar el programa de una manera masiva en una una computadora en paralelo. Esto se llama simulación paralela y distribuida. Para muchos modelos de gran escala, esta es la única manera de obtener resultados en un perı́odo de tiempo 4 razonable. La simulación de un sistema puede ser llevada a cabo a diferentes niveles de fidelidad, ya que una persona puede pensar en un modelo matemático basado en uno fı́sico, pero otra podrı́a hacerlo en un modelo más abstracto y los resultados de estos modelos serán bastante diferentes entre sı́ en cuanto a detalle. El tipo de resultado que se necesita indica el tipo de modelo que se debe emplear. Es posible preguntarse cuando la simulación debe de ser usada para estudiar sistemas dinámicos. Existen varios métodos para modelar sistemas que no involucran la simulación pero que involucran la solución de sistemas que tienen formas similares (como los sistemas de ecuaciones lineales). La simulación es a menudo esencial para los siguientes casos: 1) El modelo es muy complejo, con muchas variables y componentes que interactúan entre sı́. 2) Las relaciones entre las variables fundamentales no es lineal 3) El modelo contiene variables aleatorias 4) El resultado de la simulación es en 3D Un aspecto importante de la técnica de simulación es que se construye un modelo como réplica de un sistema real y al ejecutar la simulación, el modelo tiende a seguir la forma del método de solución más que a representar de manera precisa al sistema fı́sico, por lo que se debe de escoger un modelo que se ajuste al sistema y a las condiciones de funcionamiento de éste. Para realizar el estudio de un sistema, es difı́cil realizarlo sobre el mismo, ya que puede no estar aún construido o puede ser demasiado complicado debido a su tamaño o a su disposición, el proyecto Construcción de un Simulador Distribuido de Eventos Discretos, nos ofrece la facilidad de realizar un estudio del comportamiento de un sistema de éste tipo sin necesidad de un sistema real. Otra ventaja de este proyecto, es que al realizar la versión distribuida, podemos ejecutar la simulación de sistemas cada vez más grandes y en perı́odos de tiempo pequeños, sin la necesidad de tener una computadora muy poderosa. Para lograr la comprensión de este proyecto, hay algunos puntos que a nuestra consideración deben de ser tratados, los cuales serán descritos con la mayor claridad en los siguientes capı́tulos. 5 2. La simulación de eventos discretos Una simulación de este tipo, utiliza un modelo de estados discretos, y genera nuevos eventos gracias a las condiciones iniciales y a algunas propiedades estadı́sticas. Este tipo de simulación es muy utilizada para sistemas de cómputo y de comunicaciones, ya que pueden ser representados como un conjunto de estados discretos. Un punto importante a remarcar es que el término discreto no se aplica a los valores de tiempo utilizados en la simulación, ya que los valores de tiempo pueden ser discretos o continuos. En la simulación de eventos discretos, es necesario realizar un modelo del sistema real, llamado Red de procesos fı́sicos que consiste en vértices y aristas. Los vértices representan unidades autónomas de procesamiento mientras que las aristas son las posibles interacciones que existen entre ellas. Al contar ya con esta información, los datos más importantes que se deben de generar son el reloj y la lista de eventos. El reloj es una variable que registra el tiempo hasta el que se ha simulado, mientras que la lista de eventos nos informa de los mensajes que se han intercambiado entre las distintas unidades autónomas de procesamiento y el tiempo en el que se han dado. Al efectuar simulaciones de grandes sistemas, es necesario tener un mayor poder de procesamiento ya que una sola computadora puede tomar mucho tiempo para producir resultados. Cuando se tiene una red de computadoras, es posible realizar un método alternativo de simulación, la Simulación Distribuida de Eventos Discretos (DDES). Ésto nos permite acelerar la simulación ya que los procesos son divididos entre las computadoras de la red. Es posible observar que cuando se realiza una simulación de este tipo, al no ser ejecutados los procesos a la misma velocidad, habrá diferencias en el tiempo (las cuales se pueden corregir) y que pueden generar la posibilidad de estancamiento o interbloqueo, al haber problemas con la sincronización del tiempo. Los dos métodos más utilizados para corregir estas diferencias y permitir la sincronı́a de la simulación son : los métodos optimistas y los métodos conservadores. 3. Métodos de sincronización en la simulación Los métodos optimistas nos permiten realizar una ejecución continua de la simulación y se considera que todos los procesos se ejecutan a la misma 6 velocidad (consideración que no siempre corresponde con la realidad). De este modo, los mensajes están sincronizados y en el momento en el que son mandados, es el mismo momento en el que se espera sean recibidos. Cuando esto no sucede y se recibe un mensaje cronológicamente anterior a los que ya han sido realizados, se efectúa un mecanismo de restauración (rollback) que regresa la simulación hacia el estado correspondiente al mensaje tardı́o, para que éste pueda ser procesado. Los métodos conservadores permiten que la simulación avance en sincronı́a, ya que para avanzar en la variable de tiempo, es necesario tener la certeza de que ningún mensaje será enviado de manera tardı́a, de este modo, todos los relojes avanzan a la misma velocidad y no se tendrán problemas de estancamiento o interbloqueo. La programación de un simulador con estas caracterı́sticas es realizable gracias a una herramienta de programación tal como MPI (Message-Passing Interface), que es una interfaz de paso de mensajes. 4. Algoritmo DFS Para la realización de este proyecto, se tomó un algoritmo DFS (Depth First Search) previamente programado y a continuación se presentan los conceptos más importantes del funcionamiento de este algoritmo. En los sistemas distribuidos, el explorar y recorrer una gráfica que representa a una red, resulta ser una operación muy común. El algoritmo DFS ayuda a construir un árbol generador, que incluya a todos los nodos de la red con el mı́nimo número de aristas. De esta manera, cuando se envı́an mensajes entre los diferentes nodos, podemos aseguranos de que cada nodo recibe cada mensaje exactamente una vez. En este proyecto, se utilizó un algoritmo DFS distribuido, que supone que cada nodo del grafo lo ejecutará las mismas instrucciones. Cuando se da inicio al algoritmo, se elige a alguno de los nodos para arrancar en él el algoritmo (s), y esto es lo único que diferenciará la ejecución en los diferentes nodos. Al nodo inicial (s) se le envı́a una “ficha” o mensaje de activación (mensaje DESCUBRE) y este a su vez selecciona a otro de los nodos paa otorgarle el control de la ejecución. Sólo a aquel que tenga la “ficha” en su poder, se le considera activo, por lo que en cualquier instante del algoritmo, existe un solo proceso en estas condiciones. 7 Cada proceso que recibe un mensaje de activación, marca al emisor como su padre y, a su vez, selecciona un camino por el cual expedirá la “ficha”, para iniciar la ejecución en otro nodo. Ası́ mismo, cada vez que un proceso recibe el mensaje de DESCUBRE envı́a a sus vecinos (que no sean su padre) un mensaje de AVISO indicándoles que él ya no puede ser descubierto, por que ya tiene un padre. Si algún proceso recibe repetido un mensaje de activación desde algún vecino que ignore sus condiciones locales, el receptor deberá responder con un mensaje RECHAZO que indique su estado y la imposibilidad de ser definido como hijo de otro nodo. Finalmente, cuando un nodo ya no puede transmitir la “ficha”, ya sea porque no tiene vecinos o porque todos sus vecinos ya han sido descubiertos, enviará de vuelta a su padre un mensaje (REGRESA) para devolverle el control de las operaciones. El receptor del mensaje REGRESA designará al emisor como su hijo. A continuación se presenta el pseudocódigo de este algoritmo: <1> al recibir DESCUBRE desde el enlace j efectúa <2> si visitado(i) entonces <3> envı́a RECHAZO a j <4> sin visitar(i) <- sin visitar(i) - {j} <5> otro <6> visitado(i) <- verdadero <7> padre(i) <- j <8> envia AVISO a todos los vecinos que no sean padre(i) <9> sin visitar(i) <- sin visitar(i) - {j} <10> si existe k en sin visitar(i) entonces <11> envia DESCUBRE a k <12> sin visitar(i) <- sin visitar(i) - {k} <13> otro si padre(i) == i entonces <14> termina <15> otro <16> envia REGRESA a padre(i) <17> al recibir REGRESA o RECHAZO desde el enlace j efectua <18> ve a <10> <19> al recibir AVISO desde el enlace j efectua <20> visitado(j) <- VERDADERO <21> sin visitar(i) <- sin visitar(i) - {j} 8 5. Funcionamiento del simulador utilizado El simulador que se utilizó para este proyecto, utiliza tres tipos de mensaje: AVISO, DESCUBRE y REGRESA. El mensaje de AVISO es el primero que se envı́a desde un nodo a sus nodos vecinos y sirve para verificar si estos han sido o no descubiertos. Si se tiene uno o más vecinos sin descubrir, se elige alguno de ellos de manera aleatoria. Una vez seleccionado el nodo a descubrir, se le envı́a el mensaje Descubre, y de este modo se va recorriendo la gráfica hasta llegar al punto en que el nodo no tenga vecinos que no hayan sido previamente descubiertos, entonces comienza a enviar el mensaje Regresa al nodo que lo descubrió. Y esto se realiza hasta llegar al nodo de inicio. A continuación se estudiará un ejemplo para ver de manera más clara la ejecución del algoritmo DFS. 2 4 2 3 1 1 3 1 2 5 Figura 1: Trayectoria de los mensajes DESCUBRE Como se puede observar en la Figura 1, podemos ver que el nodo de inicio es el 1, y se analizará iteración por iteración: 1. Al tener dos vecinos escoge al azar al nodo 3 y le envı́a un mensaje DESCUBRE. Éste, al recibir el mensaje DESCUBRE envı́a un mensaje de AVISO a todos sus vecinos que no sean su padre (al 4 y al 5). 2. El nodo 3 al tener dos vecinos, le manda el mensaje DESCUBRE al nodo 5. El nodo 5 al ser descubierto, envia un AVISO al nodo 4. 9 2 4 2 3 1 1 3 1 2 5 Figura 2: Trayectoria de los mensajes DESCUBRE y REGRESA 3. El nodo 5 sólo tiene un vecino que no ha sido descubierto (el nodo 4) y le manda el mensaje DESCUBRE. El nodo 4, cuando recibe el mensaje, envia un AVISO al nodo 3. 4. El nodo 4 no tiene ningún vecino sin descubrir, ası́ que envı́a el mensaje REGRESA al nodo que lo descubrió. 5. El nodo 5 envı́a el mensaje REGRESA al nodo 3. 6. El nodo 3 envı́a el mensaje REGRESA al nodo 1. 7. El nodo 1 aún tiene un vecino sin descubrir (el nodo 2) y le manda el mensaje DESCUBRE.(Figura 2) 8. El nodo 2 no tiene vecinos a quienes descubrir, entonces manda un mensaje REGRESA al nodo 1. El simulador utilizado, necesita como datos de entrada la gráfica de adyacencias en un archivo de texto. El número de lı́nea nos indica el número de nodo y los datos vienen por parejas en un formato a,b; donde la posición a nos indica el número de nodo vecino, y b nos indica el peso. La gráfica de adyacencias para el ejemplo anterior, es la siguiente: 2,2 3,1 1,2 10 1,1 4,3 5,2 3,3 5,1 3,2 4,1 El simulador al ejecutarlo, da la siguiente salida. 1@0 2@1 3@1 3@1 4@2 5@2 5@2 4@3 4@3 3@4 5@4 3@5 1@6 2@7 1@8 DESCUBRE from 1 AVISO from 1 AVISO from 1 DESCUBRE from 1 AVISO from 3 AVISO from 3 DESCUBRE from 3 AVISO from 5 DESCUBRE from 5 AVISO from 4 REGRESA from 4 REGRESA from 5 REGRESA from 3 DESCUBRE from 1 REGRESA from 2 11 Parte II PROGRAMACIÓN DISTRIBUIDA 6. 6.1. Herramientas de la simulación distribuida MPI (Message-Passing Interface) MPI es la especificación estándar para las librerı́as de paso de mensajes. Existen varias implementaciones diferentes para las librerı́as de MPI; las más utilizadas son: MPICH, LAM/MPI, MP-MPICH, WMPI, MacMPI. MPICH es una implementación portable de la especificación estándar MPI-1 que sirve para una gran variedad de entornos computacionales paralelos y distribuidos. MPICH contiene, junto con la librerı́a MPI, un entorno de programación para trabajar con programas en MPI. El entorno de programación incluye un mecanismo de inicio portable, varias librerı́as para estudiar el desempeño de programas en MPI, y una interfase X para todas las herramientas. Funciona en un amplio conjunto de plataformas y sistemas operativos, incluyendo Unix, Windows NT y Windows 2000/XP Professional. Esta implementación se puede obtener de la dirección: http://www-unix.mcs.anl.gov/mpi/mpich/download.html LAM (Local Area Multicomputer) es un entorno de programación y de desarrollo de sistemas para una red de computadoras heterogéneas. Con LAM, un cluster dedicado o la infraestructura de una red de computadoras puede actuar como una computadora paralela para resolver un problema. Funciona en redes de estaciones de trabajo Unix/Posix. Esta implementación se puede obtener de la dirección: http://www.lam-mpi.org/7.0/download.php MP-MPICH funciona en sistemas Unix, Windows NT y Windows 2000 / XP Professional. Esta implementación se puede obtener de la dirección: http://www.lfbs.rwth-aachen.de/mp-mpich/download form.html WMPI funciona en plataforma Windows 95, 98, ME, NT y 2000. Esta implementación se puede obtener de la siguiente dirección de internet: http://www.criticalsoftware.com/hpc/ 12 MacMPI es una implementación parcial de MPI para computadoras Macintosh. Esta puede ser conseguida en la siguiente dirección: http://home.t-online.de/home/froehling/MacMPI/MacMPICW11.hqx Todas estas diferentes implementaciones tienen diferencias en los sistemas sobre los cuales pueden funcionar, pero utilizan la misma librerı́a estándar MPI. Para el simulador que se quiere realizar, decidimos que puede ser desarrollado utilizando las herramientas LAM/MPI o MPICH. 6.1.1. INSTALACIÓN DE MPICH Para instalar MPICH es necesario conseguir el código fuente (mpich.tar.gz) y compilarlo; este se puede bajar de la siguiente dirección: http://www-unix.mcs.anl.gov/mpi/mpich/ El archivo mpich.tar.gz es un paquete de archivos comprimidos, ası́ que el primer paso será descomprimirlo. Hay que colocar el archivo en el directorio donde se vaya a instalar MPICH y ejecutar: ...]$ tar -zxvf mpich.tar.gz Esto descomprimirá los archivos, creando una nueva carpeta mpich-1.2.5. A continuación, es necesario compilar el programa: ...]$ ...]$ ...]$ ...]$ cd mpich-1.2.5 ./configure make make install Después de estos pasos, MPICH estará listo para utilizarse. Al instalarlo en varias computadoras, se recomienda instalarlo en todas en la misma ruta o utilizar NFS, para evitar instalarlo más de una vez. 6.1.2. Instalación de LAM / MPI Hay dos opciones para instalar LAM / MPI: 1) Si se tiene un sistema RedHat 7.0 o mayor, o alguna otra distribución de Linux de versión equivalente con soporte para RPM’s, se puede conseguir una versión de LAM / MPI ya compilada y empaquetada en un RPM en la página 13 http://www.lam-mpi.org Una vez teniendo el archivo, sólo será necesario ejecutar la siguiente instrucción: ...]$ rpm -i lam-6.5.9-tcp.1.i386.rpm Con esto, LAM / MPI quedará instalado, si se cuenta con los paquetes necesarios para que LAM / MPI funcione. Para desinstalar el RPM de LAM / MPI sólo es necesario ejecutar lo siguiente: ...]$ rpm -e lam-6.5.9-tcp.1.i386 2) De otra manera, se puede conseguir el código fuente de LAM / MPI y compilarlo personalmente. De la misma manera, este paquete se consigue en la dirección http://www.lam-mpi.org . El archivo se llama lam-6.5.9.tar.gz y deberá ser colocado en el directorio donde se desee instalar. Una vez ahı́, es necesario descomprimir y compilar: ...]$ tar -zxvf lam-6.5.9.tar.gz ...]$ cd lam-6.5.9 ...]$ ./configure --prefix=/path/to/install/in [ si se desea instalar en el directorio actual, se puede eliminar la opción --prefix ] ...]$ make ...]$ make install 6.2. Configuración final ¿Cómo indicar en que máquinas se va a trabajar? Primero se necesita tener instalado el servidor y cliente RSH (que viene en todas las distribuciones modernas de LINUX y UNIX) en todas las máquinas en donde se va a trabajar. Una vez instalados, se necesita crear un archivo .rhosts en el home del usuario. El archivo .rhosts debe tener el nombre de cada máquina que va a utilizarse para ejecutar programas en paralelo seguido del nombre del usuario, separados por un tabulador. Ejemplo: 14 quetzal1.uam.mx quetzal2.uam.mx quetzal3.uam.mx quetzal4.uam.mx quetzal5.uam.mx . . . daco daco daco daco daco No importa si en este archivo se tienen más máquinas que aquellas con las que se pretende trabajar; el servicio rsh simplemente permite que un usuario se pueda conectar de una máquina a otra sin tener que introducir contraseña si fue previamente autorizado (en el archivo .rhosts). En algunos casos, es necesaria la creación o la modificación del archivo /etc/hosts.equiv y debe de ser editado como el ejemplo anterior. 6.2.1. MPICH Una vez instalado MPICH, es necesario indicar los nombres de las máquinas que van a participar en la ejecución de nuestros programas. Examinando el archivo: ...]$ vi /.../mpich-1.2.5/util/machines/machines.LINUX Veremos que por default, al concluir la instalación se toma en cuenta que la máquina local es la única que participará en la ejecución, varias veces. Es necesario introducir ahı́ cada una de las máquinas participantes. Si se desea que en alguna máquina se ejecute el proceso más de una vez, se vuelve a escribir su nombre en otra (u otras) lı́neas. El nombre correcto de cada máquina puede ser obtenido ejecutando en ella el comando hostname. 6.2.2. MPI/LAM Para indicar en que máquinas se va a trabajar con MPI / LAM, es necesario definir un archivo de texto con el nombre de cada máquina que va a trabajar en una lı́nea diferente y luego ejecutar la siguiente instrucción: ...]$ lamboot -v hostfile 15 Donde hostfile es el nombre del archivo con los nombres de las máquinas. Los nombres correctos de las máquinas pueden conseguirse ejecutando el comando hostname en cada una de ellas. La instrucción lamboot deja preparadas a todas las máquinas indicadas en el archivo hostfile para empezar a trabajar. Cuando se terminen de usar las máquinas con LAM / MPI, es recomendable ejecutar el comando lamhalt. 7. Compilación y ejecución de programas. Las dos implementaciones de MPI que estamos tratando coinciden en la manera de compilar y ejecutar un programa. Para compilar un programa en C y C++ se utilizan las mismas instrucciones que con el compilador gcc de Linux. Ejemplo en C: ...]$ mpicc -o <nombre del ejecutable><nombre del fuente .c> Ejemplo en C++: ...]$ mpiCC -o <nombre del ejecutable><nombre del fuente .cc> Una vez compilado el programa, se puede ejecutar en una o más máquinas de la red. Para esto es necesario ejecutar la siguiente instrucción: ...]$ mpirun -np <número de máquinas><nombre del ejecutable> 8. Comunicación básica con MPI A continuación se explicarán algunas de las funciones de MPI para realizar la comunicación entre procesadores y posteriormente, se presentan algunos ejemplos de su utilización. 8.1. Funciones de MPI void Init(int& argc, char**& argv): 16 Esta función se encarga de crear un “comunicador” entre todos los procesos que se iniciaron en las diferentes máquinas. Los argumentos que recibe son argc y argv, que vienen de la lı́nea de comando a la hora de arrancar el programa. int Comm::Get rank() const: Esta función regresa el número o indentificador (entero) del proceso que hizo el llamado de esta función. El “rank” cero se le asigna al nodo coordinador (desde donde se arrancó el programa) y numera en orden ascendente según el orden de iniciación de cada proceso en las demás máquinas (que depende a su ves del archivo de hosts incluidos en la red). int Comm::Get size() const: Esta función regresa el número de procesos que se iniciaron en total en las diferentes máquinas. void MPI::Comm::Bcast(void* buffer, MPI::Datatype& datatype, int root): int count, const Esta función hace la difusión (broadcast) de un mensaje a todos los demás procesos de la red. El primer argumento es el buffer que se va a enviar. El segundo argumento es el número de elementos dentro del buffer que se van a mandar. El tercero define el tipo de dato que se enviará. El último argumento define de que nodo parte el mensaje. Si el número coincide con el identificador del proceso, quiere decir que este nodo será el. Si el número no coincide con el identificador del proceso entonces indica el que envı́e el mensaje; si el número no coincide, quiere decir que se recibirá un mensaje del nodo indicado por ese número. void MPI::Comm::Barrier() const = 0: Esta instrucción simplemente detiene todos los procesos hasta que todos lleguen al mismo punto de ejecución. 17 8.2. Envı́o y recepción de mensajes Ejemplo de envı́o y recepción de un mensaje: #include <iostream> #include <mpi.h> using namespace std; int main(int argc, char *argv[]) { int num; int rank, size, tag, dest, from=0, tag=1; char cad[30]; MPI::Status status; MPI::Init(argc, argv); rank = MPI::COMM WORLD.Get rank(); size = MPI::COMM WORLD.Get size(); cout << “Process # ” << rank << “ saluda” << endl; if (rank == 0) { cout << “Enter the msg: ”; cin >> cad; for (dest=1;dest<size;dest++) MPI::COMM WORLD.Send(cad,5, MPI::CHAR,dest, tag); } if (rank!=0) { MPI::COMM WORLD.Recv(cad,5, MPI::CHAR,from,tag); cout << “Process ” << rank << “ received ” << cad << endl; } MPI::COMM WORLD.Barrier(); cout << “Process ” << rank << “ exiting” << endl; MPI::Finalize(); return 0; } 8.3. Otras funciones de MPI void Comm::Send(const void* buf, int count, const Datatype& datatype, int dest, int tag) const 18 Esta instrucción envı́a un mensaje almacenado en el buffer definido por const void* buf, del tamaño que es indicado por int count y de tipo especificado por const Datatype& datatype al proceso cuyo identificador sea igual que int dest. El quinto argumento es una etiqueta que tiene este mensaje, que tiene que coincidir en el envı́o y la recepción para que se pueda dar la transmisión. void Comm::Recv(void* buf, int count, const Datatype& datatype, int source, int tag) const Esta función es la contraparte de la función Send. Sus argumentos todos son iguales, a excepción del cuarto, que en vez de ser el nodo al que debe enviarse el mensaje es el nodo de donde proviene el mensaje. void Finalize() Esta función cierra el “comunicador” que se abrió con la función Init. 8.4. Ejemplo de Broadcast Broadcast de un mensaje : #include <iostream> #include <mpi.h> using namespace std; int main(int argc, char *argv[]) { int num; int rank, size, tag, next, from; char cad[30]; MPI::Status status; MPI::Init(argc, argv); rank = MPI::COMM WORLD.Get rank(); size = MPI::COMM WORLD.Get size(); cout << “Process # ” << rank << “ saluda ” << endl; if (rank == 0) { cout << “Enter the msg: ”; cin >> cad; MPI::COMM WORLD.Bcast(cad,5,MPI::CHAR,0); } 19 if (rank!=0) { MPI::COMM WORLD.Bcast(cad,5,MPI::CHAR,0); cout << “Process ” << rank << “ received ” << cad << endl; } MPI::COMM WORLD.Barrier(); cout << “Process ” << rank << “ exiting” << endl; MPI::Finalize(); return 0; } 20 Parte III PROCEDIMIENTO Y RESULTADOS 9. El algoritmo DFS distribuido Para desarrollar el algoritmo DFS distribuido, es necesario dividir la gráfica, de manera que cada procesador que participe simulará un número de nodos, que es fracción del tamaño total de la gráfica. El algoritmo DFS empieza mandando un mensaje DESCUBRE a uno de los nodos de la gráfica; este nodo inicial tendrá que estar simulado forzosamente en el procesador maestro, para que él comience su simulación mientras los otros esperan. Cuando un mensaje tenga que salir a un nodo que está en otra de las secciones de la gráfica arrancará la simulación en el procesador encargado de simular esa parte del grafo. Para hacer la versión distribuida del algoritmo DFS, hay que tomar en cuenta que no existe la posibilidad de que los diferentes procesadores se desincronicen. Esto es por que un nodo define a sus vecinos como sus hijos y espera a que ellos a su vez definan a sus hijos y regresen un mensaje cuando hayan terminado. Por esta razón nunca sucederá que un nodo reciba un mensaje con una etiqueta de tiempo anterior al tiempo actual del procesador en el que se esta simulando. Entonces, se utilizará el método optimista para desarrollar la versión distribuida de este algoritmo. A demás, dado que no existe la posibilidad de que un nodo reciba mensajes “del pasado” no será necesario hacer “rollback” para corregir los errores ocasionados por esto. 10. Adaptación del código existente En el simulador que se tenı́a, la gráfica que se va a simular, se alimenta al programa por medio de un archivo de texto. Para realizar la simulación distribuida, es necesario que todos los procesadores participantes, conozcan la gráfica y sepan cuales de los nodos de la gráfica les toca simular. Para esto, se hará uso de otro archivo de entrada que será una tabla en donde se indique 21 en que procesador se simulará cada nodo. A este archivo le llamaremos la “tabla de asignación de procesadores”. Ya que todos los procesadores participantes cuenten con la gráfica y la tabla de asignación de procesadores, el procesador maestro (con rank cero) puede empezar la simulación, al enviar un mensaje de DESCUBRE al primer nodo de la gráfica. Mientras tanto, los demás procesadores, esperan a que llegue algún mensaje a alguno de los nodos que ellos están simulando. La simulación que se lleva a cabo en el nodo maestro es igual que en la versión no distribuida, siempre y cuando los destinos de los mensajes que tenga que enviar sean nodos que él este procesando. En el momento en el que uno de los nodos a los que tenga que enviar algo no esté siendo simulado por él, tendrá que enviar un mensaje externo (utilizando MPI) y eso hará que otro de los procesadores comience a simular. Esto continuará funcionando ası́, hasta que eventualmente todos los procesadores “despierten” y realicen la parte de la simulación que les corresponde. Si en algún momento uno de los procesadores se queda sin trabajo, quedará en espera de recibir algún otro mensaje de los nodos vecinos que se estén simulando en otro procesador. Los procesadores que no son el maestro terminarán su trabajo cuando todos los nodos que esta simulando hayan enviado un mensaje REGRESA a su respectivo padre. El procesador maestro terminará cuando todos excepto el nodo inicial hayan mandado este mismo mensaje. Para realizar la adaptación del código que ya se tenı́a, se intento hacerlo modificándolo en la menor medida posible esto, con el objeto de que en un futuro se facilite la simulación de otros algoritmos en este simulador. Para ello, se comenzó por implementar una nueva clase que se encargarı́a de toda la comunicación con MPI entre los diferentes procesadores (MPIComm). El constructor de esta clase MPIComm::MPIComm(int argc, char **argv) se encarga de inicializar MPI creando el comunicador y de asignar los valores rank (número de procesador) y size (número total de procesadores que participarán). Primero, se necesitaba de un método que transmitiera los archivos con el gráfico y la tabla de asignación de procesadores a todos los procesadores participantes en la simulación. Este método tiene como prototipo: void MPIComm::FBcast(int argc, char **filenames). Lo que hace este método es hacer un “broadcast” de los dos archivos hacia todos los demás procesadores. Estos lo reciben y crean dos archivos para guardarlos. 22 Para continuar, se necesitó implementar un método que pusiera en espera a los procesadores que no fueran el maestro mientras no recibieran un primer mensaje. Este método es: Event *MPIComm::FirstMsg(). Este método utiliza una función de MPI que espera a recibir un mensaje, y detiene la ejecución hasta que éste llegue. Cuando el mensaje llega, lo empaqueta en un objeto de tipo Event y lo regresa para que se pueda iniciar la simulación en este procesador. El método void MPIComm::SendMsg(Event *e, int target)se encarga de mandar los mensajes externos hacia otros procesadores. El método recibe como parámetros el Evento (que tiene el mensaje que se va a enviar) y el destino. Para poder enviar el mensaje, lo convierte en un arreglo de cuatro flotantes. Se implemento un método que se encarga de recibir los mensajes externos: Event * MPIComm::RecvMsg(). Este método recibe el mensaje como un arreglo de flotantes y lo convierte a un objeto de tipo Event. Si la primera casilla del arreglo que se recibe corresponde al código ASCII del caracter ’F’, quiere decir que el procesador que envió el mensaje esta indicando que ha terminado de simular a todos los nodos que el manejaba. Se implementaron dos métodos, int MPIComm::getRank() y int MPIComm::getSize() que regresan el valor de size y el rank para cada procesador. El método void MPIComm::end() se utiliza para que los procesadores indiquen al maestro (rank cero) que ya han terminado de simular a todos los nodos que le correspondı́an. Para esto, envı́a un arreglo de cuatro flotantes, en donde la primera casilla es corresponde al código ASCII del caracter ’F’. Se desarrollo otro método llamado int MPIComm::finished() que regresa el valor de la variable ended. En esta variable se cuentan los procesadores que han terminado de simular a todos sus nodos. Este método sólo será utilizado por el maestro. Finalmente, el destructor de esta clase MPIComm:: MPIComm() se encarga de finalizar la comunicación con MPI cerrando el comunicador. Una vez contando con la clase MPIComm se procedió a modificar el código existente de manera que se utilizara a esta nueva clase para hacer la comunicación externa entre los diferentes procesadores. 23 Para comenzar, se modificó la clase Graph, de manera que también cree una estructura de datos (lista ligada) con la tabla de asignación de procesadores. Ası́ mismo, se implementó un método int Graph::getRank(int pos) que recibe el número de un nodo y regresa el rank procesador que lo está simulando. Para ello, fue necesario modificar la clase LinkList y agregarle un método template<class T> T LinkList<T>::getNext(int posit) que regresa el elemento que se encuentra tantas posiciones adelante como lo indica el parámetro posit. Para continuar, se modificó el main, de manera que manda a llamar al método FBcast y al constructor de la clase MPIComm. Después, el procesador maestro comienza su simulación de manera normal, mientras los demás se ponen en espera de un primer mensaje, haciendo un llamado al método FirstMsg de la clase MPIComm. Cuando cada procesador recibe su primer mensaje, lo inserta en la cola de eventos y comienza su simulación. También se modificó la clase DFS de manera que el método virtual int receive(Event *e,Graph myNetwork) devuelve el valor uno cuando el nodo que se está simulando, envı́a un mensaje de REGRESA. Después se modificó el método void Simulation::run(Graph myNetwork) de la clase Simulation. Primero se cuenta el número de nodos que está simulando el procesador actual; de esta manera, se puede saber cuando el procesador ha terminado su trabajo, al contar también el número de mensajes tipo REGRESA que se han enviado. Si se trata del procesador maestro, se resta uno al número de nodos simulados, por que el nodo inicial nunca manda un mensaje REGRESA. Luego, se revisa si hay un mensaje externo haciendo una llamada al método MPIComm::RecvMsg y si lo hay se inserta en la cola de eventos. Después, en caso de que haya eventos en la cola, se procede a desencolarlo y atenderlo. El método int Process::receive(Event *e,Graph myNetwork) devolverá el valor uno cuando se haya enviado un mensaje REGRESA; se cuenta cuantos mensajes de este tipo han aparecido en la variable fin. En los nodos que no son el maestro, cuando este número coincida con el número de nodos que se están simulando en el procesador actual, se habrá terminado la simulación; se saldrá del método run, para regresar al main y se mandará a rank cero un mensaje indicando que ya terminó (utilizando el método MPIComm::end()). El procesador maestro, terminará sólo cuando todos los demás procesadores hayan terminado (para esto utiliza los métodos MPIComm::finished() y MPIComm::getSize()) y además, ya todos los nodos que él este simulando 24 Figura 3: Grafo (excepto el inicial) hayan mandado el mensaje REGRESA. 11. Resultados Finales Se corrió el algoritmo DFS distribuido en un grafo con seis nodos, utilizando diferentes tamaños de particiones. A continuación se presenta el resultado de una corrida del simulador DFS normal y después, los resultados de las diferentes corridas del algoritmo distribuido. El grafo que se utilizó es el que se muestra en la figura 3. 1. Ejecución en un solo procesador. El grafo no se particiona (figura 4). Resultado: tamano de la red 6 Rank 0: 1@0 DESCUBRE from 1 Rank 0: 4@1 AVISO from 1 Rank 0: 2@1 AVISO from 1 Rank 0: 3@1 AVISO from 1 Rank 0: 4@1 DESCUBRE from 1 Rank 0: 2@2 AVISO from 4 Rank 0: 3@2 AVISO from 4 Rank 0: 5@2 AVISO from 4 25 Figura 4: Grafo sin partición Rank Rank Rank Rank Rank Rank Rank Rank Rank Rank Rank Rank Rank Rank Rank Rank Rank Rank 0: 0: 0: 0: 0: 0: 0: 0: 0: 0: 0: 0: 0: 0: 0: 0: 0: 0: 6@2 AVISO from 4 2@2 DESCUBRE from 4 1@3 AVISO from 2 3@3 AVISO from 2 3@3 DESCUBRE from 2 1@4 AVISO from 3 4@4 AVISO from 3 5@4 AVISO from 3 5@4 DESCUBRE from 3 4@5 AVISO from 5 6@5 AVISO from 5 6@5 DESCUBRE from 5 4@6 AVISO from 6 5@6 REGRESA from 6 3@7 REGRESA from 5 2@8 REGRESA from 3 4@9 REGRESA from 2 1@10 REGRESA from 4 En esta ejecución, todos los mensajes que se envı́an los nodos son “mensajes internos”; es decir, como hay un solo procesador simulando, no se utiliza MPI para enviar mensajes entre procesadores. 2. Ejecución en dos procesadores. El grafo se particiona como se observa 26 Figura 5: Grafo particionado en dos en la figura 5. Resultado: Rank 1: 4@1 AVISO from 1 Rank 1: 2@1 AVISO from 1 Rank 1: 4@1 DESCUBRE from 1 Rank 1: 2@2 AVISO from 4 Rank 1: 6@2 AVISO from 4 Rank 1: 2@2 DESCUBRE from 4 Rank 1: 4@4 AVISO from 3 Rank 1: 4@5 AVISO from 5 Rank 1: 6@5 AVISO from 5 Rank 1: 6@5 DESCUBRE from 5 Rank 1: 4@6 AVISO from 6 Rank 1: 2@8 REGRESA from 3 Rank 1: 4@9 REGRESA from 2 tamano de la red 6 Rank 0: 1@0 DESCUBRE from 1 Rank 0: 3@1 AVISO from 1 Rank 0: 3@2 AVISO from 4 Rank 0: 5@2 AVISO from 4 Rank 0: 1@3 AVISO from 2 Rank 0: 3@3 AVISO from 2 27 Figura 6: Grafo particionado en tres Rank Rank Rank Rank Rank Rank Rank 0: 0: 0: 0: 0: 0: 0: 3@3 DESCUBRE from 2 1@4 AVISO from 3 5@4 AVISO from 3 5@4 DESCUBRE from 3 5@6 REGRESA from 6 3@7 REGRESA from 5 1@10 REGRESA from 4 Se puede identificar en que procesador se generó cada mensaje. Cuando el nodo fuente y el nodo destino de un mensaje no se están simulando en un mismo procesador se envı́a un mensaje externo utilizando MPI. 3. Ejecución en tres procesadores. El grafo se particiona como se observa en la figura 6. Resultado: Rank Rank Rank Rank Rank Rank Rank Rank 1: 1: 1: 1: 1: 1: 1: 1: 2@1 2@2 5@2 2@2 5@4 5@4 5@6 2@8 AVISO from 1 AVISO from 4 AVISO from 4 DESCUBRE from 4 AVISO from 3 DESCUBRE from 3 REGRESA from 6 REGRESA from 3 28 Figura 7: Grafo particionado en cuatro Rank 2: 3@1 AVISO from 1 Rank 2: 3@2 AVISO from 4 Rank 2: 6@2 AVISO from 4 Rank 2: 3@3 AVISO from 2 Rank 2: 3@3 DESCUBRE from 2 Rank 2: 6@5 AVISO from 5 Rank 2: 6@5 DESCUBRE from 5 Rank 2: 3@7 REGRESA from 5 tamano de la red 6 Rank 0: 1@0 DESCUBRE from 1 Rank 0: 4@1 AVISO from 1 Rank 0: 4@1 DESCUBRE from 1 Rank 0: 1@3 AVISO from 2 Rank 0: 1@4 AVISO from 3 Rank 0: 4@4 AVISO from 3 Rank 0: 4@5 AVISO from 5 Rank 0: 4@6 AVISO from 6 Rank 0: 4@9 REGRESA from 2 Rank 0: 1@10 REGRESA from 4 4. Ejecución en cuatro procesadores. El grafo se particiona como se observa en la figura 7. Resultado: 29 Rank 1: 2@1 AVISO from 1 Rank 1: 2@2 AVISO from 4 Rank 1: 6@2 AVISO from 4 Rank 1: 2@2 DESCUBRE from 4 Rank 1: 6@5 AVISO from 5 Rank 1: 6@5 DESCUBRE from 5 Rank 1: 2@8 REGRESA from 3 Rank 3: 4@1 AVISO from 1 Rank 3: 4@1 DESCUBRE from 1 Rank 3: 4@5 AVISO from 5 Rank 3: 4@6 AVISO from 6 Rank 3: 4@4 AVISO from 3 Rank 3: 4@9 REGRESA from 2 Rank 2: 3@1 AVISO from 1 Rank 2: 3@2 AVISO from 4 Rank 2: 3@3 AVISO from 2 Rank 2: 3@3 DESCUBRE from 2 Rank 2: 3@7 REGRESA from 5 tamano de la red 6 Rank 0: 1@0 DESCUBRE from 1 Rank 0: 1@3 AVISO from 2 Rank 0: 5@2 AVISO from 4 Rank 0: 1@4 AVISO from 3 Rank 0: 5@4 AVISO from 3 Rank 0: 5@4 DESCUBRE from 3 Rank 0: 5@6 REGRESA from 6 Rank 0: 1@10 REGRESA from 4 Dado el grafo que se utilizo y la partición que se hizo, todos los mensajes entre nodos tendrán que ser externos. Esto es, por que no se esta simulando en un mismo procesador dos nodos que sean vecinos. Se podrı́a modificar las particiones de manera que hubiera algunos nodos que pudieran mandar mensajes internos. 5. Ejecución en cinco procesadores. El grafo se particiona como se observa en la figura 8. Resultado: 30 Figura 8: Grafo particionado en cinco Rank 1: 2@1 AVISO from 1 Rank 1: 2@2 AVISO from 4 Rank 1: 2@2 DESCUBRE from 4 Rank 1: 2@8 REGRESA from 3 Rank 3: 4@1 AVISO from 1 Rank 3: 4@1 DESCUBRE from 1 Rank 3: 4@4 AVISO from 3 Rank 3: 4@5 AVISO from 5 Rank 3: 4@6 AVISO from 6 Rank 3: 4@9 REGRESA from 2 Rank 2: 3@1 AVISO from 1 Rank 2: 3@2 AVISO from 4 Rank 2: 3@3 AVISO from 2 Rank 2: 3@3 DESCUBRE from 2 Rank 2: 3@7 REGRESA from 5 Rank 4: 5@2 AVISO from 4 Rank 4: 5@4 AVISO from 3 Rank 4: 5@4 DESCUBRE from 3 Rank 4: 5@6 REGRESA from 6 tamano de la red 6 Rank 0: 1@0 DESCUBRE from 1 Rank 0: 6@2 AVISO from 4 Rank 0: 1@3 AVISO from 2 Rank 0: 1@4 AVISO from 3 31 Figura 9: Grafo particionado en seis Rank 0: 6@5 AVISO from 5 Rank 0: 6@5 DESCUBRE from 5 Rank 0: 1@10 REGRESA from 4 Igual que en el caso anterior, todos los mensajes entre nodos resultan ser externos. Esto es, por que no se esta simulando en un mismo procesador dos nodos que sean vecinos. 6. Ejecución en seis procesadores. El grafo se particiona como se observa en la figura 9. Resultado: Rank Rank Rank Rank Rank Rank Rank Rank Rank Rank Rank Rank 1: 1: 1: 1: 5: 5: 5: 2: 2: 2: 2: 2: 2@1 2@2 2@2 2@8 6@2 6@5 6@5 3@1 3@2 3@3 3@3 3@7 AVISO from 1 AVISO from 4 DESCUBRE from 4 REGRESA from 3 AVISO from 4 AVISO from 5 DESCUBRE from 5 AVISO from 1 AVISO from 4 AVISO from 2 DESCUBRE from 2 REGRESA from 5 32 Rank 4: 5@2 AVISO from 4 Rank 4: 5@4 AVISO from 3 Rank 4: 5@4 DESCUBRE from 3 Rank 4: 5@6 REGRESA from 6 Rank 3: 4@1 AVISO from 1 Rank 3: 4@1 DESCUBRE from 1 Rank 3: 4@4 AVISO from 3 Rank 3: 4@5 AVISO from 5 Rank 3: 4@6 AVISO from 6 Rank 3: 4@9 REGRESA from 2 tamano de la red 6 Rank 0: 1@0 DESCUBRE from 1 Rank 0: 1@3 AVISO from 2 Rank 0: 1@4 AVISO from 3 Rank 0: 1@10 REGRESA from 4 En este caso, dado que el número de procesadores es igual al número de nodos de la gráfica, todos los mensajes tendrán que ser externos. No hay una manera de particionar para que no suceda esto, por que cada procesador deberá simular por lo menos un nodo. 12. Observaciones y Conclusiones Se puede observar en los resultados de la sección anterior, que la salida que se obtiene en las diferentes ejecuciones es la misma, aunque los mensajes no se encuentran ordenados según su etiqueta de tiempo. Esto no es un error del algoritmo, como podrı́a parecer; lo que sucede, es que MPI redirige la salida a pantalla de los diferentes procesadores y la manda al maestro (rank 0). Esto quiere decir que el texto de salida se tiene que enviar utilizando la red, lo cual toma un cierto tiempo; entonces, cuando finalmente llega a rank 0 para ser desplegado, puede ser que el maestro ya haya mandado a desplegar algo que se haya generado más tarde, pero que le haya llegado antes. Por otra parte, cada procesador simulará a un ritmo diferente (dependiendo de su capacidad y carga de trabajo) de esta manera, un procesador puede avanzar más rápido y enviar a desplegar mensajes con etiqueta más avanzada que otro procesador más lento. Con los resultados, se puede ver la importancia de tener particiones ade33 cuadas a cada gráfica que se simule. A diferencia de lo que se podrı́a pensar, en el ejemplo que se simuló (un grafo con seis nodos), no es conveniente utilizar el número máximo de procesadores para hacer el trabajo. Si se hace esto, todos los mensajes entre nodos tienen que ser externos, utilizando la red local que comunica a las diferentes computadoras, y esto ocasionar una gran reducción de la eficiencia. En el otro extremo, utilizando un solo procesador, se tiene que todos los mensajes son internos, pero no se esta utilizando el potencial de la ejecución en paralelo. El caso óptimo, debe estar en algún punto intermedio entre estos dos extremos. Una vez habiendo escogido el número de procesadores que participarán, ahora se tiene que hacer una partición adecuada; la mejor manera de particionar será aquella en la que puedan quedar la mayor cantidad de nodos vecinos simulándose en un mismo procesador. Cabe mencionar que el uso de la herramienta para programación en paralelo MPI, hizo que la programación fuera mucho más sencilla. El uso de sockets queda encapsulado y para hacer la comunicación entre procesadores sólo se necesita hacer llamadas a funciones ya hechas. 13. Trabajo por hacer En esta sección se discutirán las perspectivas de trabajo que quedan por desarrollar alrededor de este proyecto. En el algoritmo DFS distribuido, como ya se indicó con anterioridad, no existe la posibilidad de que los diferentes procesadores se desincronicen, por lo cual no hay necesidad de implementar “roll back”. Para trabajos futuros en donde se quiera simular otros algoritmos que sı́ lo necesiten, será necesario implementar una clase que vaya guardando un historial para poder regresar la simulación a un estado en donde no haya mensajes con etiqueta de tiempo anterior a la del simulador. También será necesario modificar la clase de comunicación MPIComm de manera que se incluya el envı́o de “antimensajes” para indicar a los demás procesadores que se ha hecho un “rollback” y que todos los mensajes enviados a partir de cierto tiempo no sean tomados en cuenta. Para hacer la versión distribuida del algoritmo DFS, se tuvo que modificar la condición de paro del ciclo iterativo. En la versión normal, la condición de paro era que la cola de eventos quedara vacı́a. En la versión distribuida, cada procesador tiene una cola de eventos diferente, en donde están los 34 eventos concernientes únicamente a los nodos que él procesa. Entonces esta cola puede estar vacı́a durante mucho tiempo en un procesador, mientras los nodos que él simula no recibı́an mensajes de sus vecinos simulados en otros procesadores. La condición de paro se modificó de manera que el algoritmo termina cuando el número de mensajes REGRESA que se han enviado en un procesador sea igual al número de nodos simulados en él. Esta condición de paro, sin embargo, es especı́ficamente para el algoritmo DFS; queda pendiente desarrollar un algoritmo distribuido para calcular la condición de para de manera que no sea especı́fica para el algoritmo DFS, de manera que se puedan simular otros algoritmos de redes con este mismo simulador. En los algoritmos distribuidos se tiene que dividir el problema que se quiere resolver, de manera que cada uno de los procesadores participantes, trabajará sobre una porción del mismo. En este caso, se divide la gráfica y cada procesador simulará un número menor de nodos que el del grafo original. Lo que se hizo en este trabajo, fue pedir al usuario que indique en que procesador se simulará cada nodo. Un trabajo a futuro, puede ser desarrollar un algoritmo que se encargue de hacer la división del gráfico de manera inteligente sin que el usuario tenga que hacer este trabajo. Esto se podrı́a implementar de manera que se pueda decidir entre indicar la división del grafo con un archivo o que el simulador lo haga de manera automática. Como se comenta en la sección anterior, la manera de particionar el grafo será crucial en el desempeño del algoritmo, de manera que queda pendiente investigar una manera metódica de generar las particiones de manera que el desempeño del algoritmo distribuido sea óptimo. 35 Parte IV APÉNDICES 14. 14.1. Código fuente del simulador DFS distribuido Clase: DFS / Archivo dfs.cc #include “simulation.h” #include “linklist.cc” #include “mpi++.h” using namespace std; enum message { DESCUBRE = ’A’, REGRESA = ’B’, AVISO = ’C’ }; class DFS; class Message; class Message: public Event { private: friend class DFS; public: // constructor nulo Message() {}; // constructor inicializado: nombre, tiempo, destino, origen Message(char n, float t, int d, int o): Event(n, t, d, o) {}; // destructor ˜Message() {}; }; class DFS: public Model { private: int visitado; int padre; int fin; 36 LinkList<int> sin visitar; LinkList<int> esperando; public: virtual void init() { int i, *n; fin=0; visitado = 0; padre = me; while (n = neighbors->next()){ sin visitar.inqueue(i = *n); } }; void send(char msj, int dst,Graph myNetwork) { Event *e; e = new Message(msj, clock+1.0, dst, me); transmit(e,myNetwork); } virtual int receive(Event *e,Graph myNetwork) { Message *te; atiende((Message *) e,myNetwork); return(fin); }; void continua exploracion(Graph myNetwork) { int *h,l; if (!sin visitar.isEmpty()) { l = sin visitar.dequeue(); send(DESCUBRE, l,myNetwork); } else { if (padre != me){ fin=1; send(REGRESA, padre,myNetwork); } }; 37 }; void atiende(Message *e,Graph myNetwork){ int j = e->getSource(); switch(e->getName()){ case DESCUBRE: cout << “Rank ” << MPIComm::getRank() << “: ” << me << “@” << clock << ” DESCUBRE from “ << j << endl; sin visitar.remove(j); if (!visitado){ visitado = 1; padre = j; while (int *k = neighbors->next()) // sin visitar NO! { if (padre != *k) send(AVISO, *k,myNetwork); }; continua exploracion(myNetwork); }; break; case REGRESA: cout << “Rank ” << MPIComm::getRank() << “: ” << me << “@” << clock << “ REGRESA from ” << j << endl; continua exploracion(myNetwork); break; case AVISO: cout << “Rank ” << MPIComm::getRank() << “: ” << me << “@” << clock << “ AVISO from ” << j << endl; sin visitar.remove(j); break; }; }; }; main(int argc, char **argv) { int i,j; 38 MPIComm mpi(argc,argv); Simulator *myEngine = new Simulator(500.0); mpi.FBcast(argc,argv); Graph myNetwork(mpi.getRank()); if(mpi.getRank()==0) cout << “tamano de la red ” << myNetwork.getSize() << endl; Simulation myExperiment = Simulation(myNetwork, myEngine); for (int i = 1; i <= myNetwork.getSize(); i++) { DFS *newmodel = new DFS(); myExperiment.setModel(newmodel, i); } if (mpi.getRank()==0){ Message *e1 = new Message(DESCUBRE,0.0,1,1); myExperiment.init(e1); myExperiment.run(myNetwork); } else { Message *e1; e1=(Message *)mpi.FirstMsg(); myExperiment.init(e1); myExperiment.run(myNetwork); mpi.end(); } } 14.2. Clase: Simulation 14.2.1. Archivo simulation.h // Archivo: simulation.h // interfaz de la clase ’Simulation’ // R. Marcelin J. (02/08/98) #if !defined( SIMULATION ) #define SIMULATION #include “graph.h” #include “model.h” #include “event.h” 39 #include #include #include #include #include #include “process.h” “simulator.h” “avltree.cc” “mpicomm.h” <iostream> <fstream> class Simulation { public: Simulation(Graph graph, Simulator *engine); ˜Simulation(); // void setModel(Model *model, int id); void init(Event *e); // void run(Graph myNetwork); // private: Graph net; // AVLTree<Process> table; // Simulator *myEngine; // }; #endif 14.2.2. Archivo simulation.cc // Archivo: simulation.cc // implementacion de la clase ’Simulation’ // R. Marcelin J. (02/08/98) #include “simulation.h” //@ Simulation(): constructor //—————————————————————— Simulation::Simulation(Graph graph, Simulator *engine): net(graph) { int i, *r; Process *newproc; 40 LinkList<int> *row; myEngine = engine; for (i = 1; i <= net.getSize(); i++) { row = net.getRow(i); newproc = new Process(*row, engine, i); table.insert(*newproc, i); }; for (i = 1; i <= net.getSize(); i++) { row = net.getRow(i); while (r = row->next()) { newproc = table.find(*r); newproc->addRxNeighbor(i); }; }; } //@ ˜Simulation(): destructor //—————————————————————— Simulation::˜Simulation() {} //@ setModel(): //—————————————————————— void Simulation::setModel(Model *model, int id) { Process *proc; proc = table.find(id); proc->setModel(model); } //@ init(): //—————————————————————— void Simulation::init(Event *e) { myEngine->insertEvent(e); //@ run(): 41 } //—————————————————————— void Simulation::run(Graph myNetwork) { int t,i,fin=0,numproc=0,rank; long j=1; Event *e; Process *nextproc; rank=MPIComm::getRank(); for(i=1;i<=myNetwork.getSize();i++) if(myNetwork.getRank(i)==rank) numproc++; if(rank==0) numproc–; i=1; while(1 /*myEngine->isOn()*/){ if (e = MPIComm::RecvMsg()) myEngine->insertEvent(e); if(myEngine->isOn()){ e = myEngine->dequeueEvent(); t = e->getTarget(); nextproc = table.find(t); fin+=nextproc->receive(e,myNetwork); } if(rank==0){ if(MPIComm::finished()==MPIComm::getSize()-1) if(fin==numproc && !myEngine->isOn()) break; } else if(fin==numproc && !myEngine->isOn()) break; } } 14.3. Clase: Model 14.3.1. Archivo model.h // // // // Archivo: model.h intefaz de la clase ’Model’ R. Marcelin J. (02/08/99) esta es una version modificada que extiende la definicion de la clase 42 // MODELO, para permitir la concatenacion de automatas (29/10/01). #if !defined( MODEL ) #define MODEL #include “event.h” #include “linklist.cc” #include “graph.h” #include “mpicomm.h” //#include “intset.h” class Model { public: Model(); // constructor ˜Model(); // destructor virtual void init(); virtual void init(int pid); void transmit(Event *e,Graph myNetwork); // envia evento void transmitAll(Event *e); virtual int receive(Event *e,Graph myNetwork); // recibe event friend class Process; protected: Process *myProcess; // proceso a cargo int me; LinkList<int> *neighbors; LinkList<Model *> partners; // esta es la modificacion float clock; }; #endif 14.3.2. // // // // // Archivo model.cc Archivo: model.cc implementacion de la clase ’Model’ R. Marcelin J. (02/08/99) esta es una version modificada que extiende la definicion de la clase MODELO, para permitir la concatenacion de automatas (29/10/01). 43 #include “event.h” #include “model.h” #include “process.h” #include <iostream.h> //@ Model(): constructor //—————————————————————— Model::Model() { clock = 0; } //@ ˜Model(): destructor //—————————————————————— Model::˜Model() { } //@ init(): //—————————————————————— void Model::init() { } //@ init(): //—————————————————————— void Model::init(int pid) { //@ transmit(): transmite evento //—————————————————————— void Model::transmit(Event *e,Graph myNetwork) { myProcess->transmit(e,myNetwork); } //@ transmitAll(): //—————————————————————— void Model::transmitAll(Event *e) { myProcess->transmitAll(e); //@ receive(): recibe evento //—————————————————————— int Model::receive(Event *e,Graph myNetwork) { 44 } } } 14.4. Clase: Event 14.4.1. Archivo event.h // Archivo: event.h // interfaz de la clase ’Event’ // R. Marcelin J. (2/08/99) #if !defined( EVENT ) #define EVENT class Event { public: Event(); Event(char name, float time, int target, int source); // ˜Event(); // char getName(); // float getTime(); // int getSource(); // int getTarget(); // protected: char name; // nombre float time; // estampilla de tiempo int target; // destino int source; // origen }; #endif 14.4.2. Archivo event.cc // Archivo: event.cc // implementacion de la clase ’Event’ // R. Marcelin J. (2/08/99) #include “event.h” //@ Event(): constructor //—————————————————————— 45 Event::Event() { } //@ Event(): constructor //—————————————————————— Event::Event(char name, float time, int target, int source) { name = name; time = time; target = target; source = source; } //@ ˜Event(): destructor //—————————————————————— Event::˜Event(void) { //@ getName(): //—————————————————————— char Event::getName(void) { return name; } } //@ getTime() //—————————————————————— float Event::getTime(void) { return time; } //@ getSource() //—————————————————————— int Event::getSource(void) { return source; } //@ getTarget() //—————————————————————— int Event::getTarget(void) { return target; } 46 14.5. Clase: Process 14.5.1. Archivo process.h // Archivo: process.h // interfaz de la clase ’Process’ // R. Marcelin J. (02/08/99) // esta es una version modificada que extiende la definicion de la clase // MODELO, para permitir la concatenacion de automatas (29/10/01). #if !defined( PROCESS ) #define PROCESS #include “model.h” #include “event.h” #include “simulator.h” #include “linklist.cc” #include “graph.h” #include “mpicomm.h” //#include “intset.h” class Process { public: Process(LinkList<int> & tx, Simulator *engine, int id); ˜Process(); // void setModel(Model *model); void transmit(Event *e,Graph myNetwork); // void transmitAll(Event *e); // int receive(Event *e,Graph myNetwork); // void addTxNeighbor(int id); // void addRxNeighbor(int id); // int *getTxNeighbor(); // devuelve ap. al item, o nulo int *getRxNeighbor(); // devuelve ap. al item, o nulo friend class Model; private: int id; // Model *myModel; // Simulator *myEngine; // LinkList<int> txSet; // 47 LinkList<int> rxSet; // }; #endif 14.5.2. Archivo process.cc // Archivo: process.cc // implementacion de la clase ’Process’ // R. Marcelin J. (02/08/99) // esta es una version modificada que extiende la definicion de la clase // MODELO, para permitir la concatenacion de automatas (29/10/01). #include “process.h” #include <iostream.h> //@ Process(): constructor //—————————————————————— Process::Process(LinkList<int> & tx, Simulator *eng, int id): txSet( tx), myEngine(eng), id( id) { /*txSet = new LinkList<int>( tx);*/ } //@ ˜Process(): destructor //—————————————————————— Process::˜Process() { //@ setModel(): //—————————————————————— void Process::setModel(Model *model) { int pid; Model **m1, *m2; myModel = model; myModel->myProcess = this; myModel->me = id; myModel->neighbors = &txSet; myModel->init(); pid = 1; while (m1=(myModel->partners.next())) { 48 } m2=*m1; m2->myProcess = this; m2->me = id; m2->neighbors = &txSet; m2->init(pid++); m2->partners.insert(model, 1); }; } //@ transmit(): //—————————————————————— void Process::transmit(Event *e, Graph myNetwork) { int target,targetrank; target=e->getTarget(); targetrank=myNetwork.getRank(target); if(targetrank==MPIComm::getRank()){ myEngine->insertEvent(e); } else{ MPIComm::SendMsg(e,targetrank); } } //@ sendAll(): //—————————————————————— void Process::transmitAll(Event *e) { // int *to; // Event *ecopy; // while (to = getTxNeighbor()) // { ecopy = new Event(e->getName(), e->getTime()+1, *to, e->getSource()); // myEngine->insertEvent(ecopy); // }; } //@ receive(): 49 //—————————————————————— int Process::receive(Event *e,Graph myNetwork) { int fin; Model **m1, *m2; myModel->clock = e->getTime(); while (m1=(myModel->partners.next())) { m2=*m1; m2->clock = myModel->clock; }; fin=myModel->receive(e,myNetwork); return(fin); } //@ addTxNeighbor(): //—————————————————————— void Process::addTxNeighbor(int id) { txSet.insert( id); //@ addRxNeighbor(): //—————————————————————— void Process::addRxNeighbor(int id) { rxSet.insert( id); //@ getTxNeighbor(): //—————————————————————— int *Process::getTxNeighbor() { return txSet.next(); //@ getRxNeighbor(): //—————————————————————— int *Process::getRxNeighbor() { return rxSet.next(); 14.6. Clase: Simulator 14.6.1. Archivo simulator.h // Archivo: simulator.h // interfaz de la clase ’Simulator’ 50 } } } } // R. Marcelin J. (02/08/99) #if !defined( SIMULATOR ) #define SIMULATOR #include “event.h” #include “avltree.cc” class Simulator { public: Simulator(float d); // ˜Simulator(); // void insertEvent(Event *e); // Event *dequeueEvent(); // int isOn(); // private: float duration; float clock; // AVLTree<Event *> agenda; // }; #endif 14.6.2. Archivo simulator.cc // Archivo: simulator.cc // implementacion de la clase ’Simulator’ // R. Marcelin J. (02/08/98) #include “simulator.h” //@ Simulator(): constructor //—————————————————————— Simulator::Simulator(float d) { clock = 0.0; duration = d; } //@ ˜Simulator(): destructor //—————————————————————— Simulator::˜Simulator() 51 { //@ insertEvent(): //—————————————————————— void Simulator::insertEvent(Event *e) { float key = e->getTime(); agenda.insert(e, key); } } //@ dequeueEvent(): //—————————————————————— Event *Simulator::dequeueEvent() { Event *e; e = agenda.minval(); clock = e->getTime(); return e; } //@ isOn(): //—————————————————————— int Simulator::isOn() { return (!agenda.isEmpty())&&(clock <= duration); 14.7. Clase: Graph 14.7.1. Archivo graph.h // Archivo: graph.h // interface de la clase ’Graph’ // R. Marcelin J. (02/08/99) #if !defined( GRAPH ) #define GRAPH #include <fstream.h> #include <iostream.h> #include “mpicomm.h” #include “linklist.cc” const int NEWL = 10; 52 } class Graph { public: Graph(int rank); // constructor ˜Graph(); // destructor int getSize(); // devuelve num. vertices LinkList<int> *getRow(int id); // devuelve un renglon de ’g’ int getRank(int pos); // devuelve el elemento de la posicion dada private: int size; // taman˜o de la grafica LinkList<LinkList<int> *> g; // ’matriz’ de adyacencias LinkList<int> t; // tabla de asignacion de procesadores }; #endif 14.7.2. Archivo graph.cc // Archivo: graph.cc // implementacion de la clase ’Graph’ // R. Marcelin J. (02/08/99) #include “graph.h” //@ Graph(): constructor //—————————————————————— Graph::Graph(int rank) { char c,fgraphname[8]=“.graph ”,ftablename[8]=“.table ”; int temp; int proc; int vertice; int weight; LinkList<int> *v; fgraphname[6]=ftablename[6]=rank+’0’; ifstream ins(fgraphname); ifstream ftable(ftablename); if (!ins) { cerr << “No pude abrir el archivo{}n”; exit(1); 53 }; for (size =0; !ins.eof(); size++) { v = new LinkList<int>(); while ((ins.get(c))&&(c!=NEWL)) { ins.unget(); vertice = 0; while ((ins.get(c))&&(c!=’ ’)&&(c!=’,’)){ temp = c - ’0’; vertice=10*vertice+temp; }; if (c==’,’){ weight = 0; while ((ins.get(c))&&(c!=’ ’)&&(c!=’,’)&&(c!=NEWL)){ temp = c - ’0’; weight=10*weight+temp; }; ins.unget(); }; while ((ins.get(c))&&(c==’ ’)); ins.unget(); if (weight) v->insert(vertice, weight); else v->insert(vertice); }; g.insert(v, size + 1); }; ins.close(); if (!ftable) { cerr << “No pude abrir el archivo{}n”; exit(1); }; while(!ftable.eof()){ proc=0; while(ftable.get(c) && c!=NEWL) { if(c<’0’ —— c>’9’) { while(ftable.get(c) && c!=NEWL); ftable.unget(); } 54 else proc=10*proc+(c-’0’); } t.inqueue(proc); } ftable.close(); } //@ Graph(): destructor //—————————————————————— Graph::˜Graph() { //@ getSize(): devuelve num. de vertices //—————————————————————— int Graph::getSize() { return size; } } //@ getRow(): devuelve la lista de vecinos del vertice ’id’ //—————————————————————— LinkList<int> *Graph::getRow(int id) { return *(g.find(id)); } //@ getRank(): devuelve el elemento de la posicion dada //—————————————————————— int Graph::getRank(int pos) { return(t.getNext(pos)); } 14.8. Clase: MPIComm 14.8.1. Archivo mpicomm.h // Archivo: mpicomm.h // interface de la clase ’MPIComm’ // R. Marcelin J. (02/08/99) #if !defined( MPICOMM ) #define MPICOMM #include <fstream.h> #include <iostream.h> 55 #include “event.h” #include “mpi++.h” class MPIComm { public: //metodos: MPIComm(int argc, char **argv);// constructor MPIComm(); void FBcast(int argc, char **filenames); static int getRank(); static int getSize(); static Event *FirstMsg(); static Event *RecvMsg(); static void SendMsg(Event *e, int target); void end(); static int finished(); private: //variables: static int rank; static int size; static int ended; }; #endif 14.8.2. Archivo mpicomm.cc // Archivo: mpicomm.cc // implementacion de la clase ’MPIComm’ // R. Marcelin J. (02/08/99) #include “mpicomm.h” int MPIComm::rank; int MPIComm::size; int MPIComm::ended; //@ MPIComm(): constructor //—————————————————————— MPIComm::MPIComm(int argc, char **argv) { MPI::Init(argc,argv); rank = MPI::COMM WORLD.Get rank(); 56 size = MPI::COMM WORLD.Get size(); ended = 0; } //@ FBcast(): hace un broadcast de los archivos //—————————————————————– void MPIComm::FBcast(int argc, char **filenames) { int graphlength,tablelength; char graphoutname[8]=“.graph ”,tableoutname[8]=“.table ”,*graphbuf,*tablebuf; ifstream fgraphin,ftablein; ofstream fgraphout,ftableout; if (rank == 0){ if (argc < 3){ cout << “Por favor indique el nombre del archivo con la grafica y la tabla de asignacion de procesadores” << endl; graphlength=-10; } else{ fgraphin.open(filenames[1], ios::binary ); ftablein.open(filenames[2], ios::binary ); if(!fgraphin.good() —— !ftablein.good()){ cout << “Nombre de archivo incorrecto” << endl; graphlength=-10; } else{ fgraphin.seekg(0, ios::end); graphlength = fgraphin.tellg(); fgraphin.seekg(0, ios::beg); graphbuf = new char[graphlength]; fgraphin.read(graphbuf,graphlength); fgraphin.close(); graphbuf[graphlength-1]=’{}0’; ftablein.seekg(0,ios::end); tablelength = ftablein.tellg(); ftablein.seekg(0,ios::beg); tablebuf = new char[tablelength]; ftablein.read(tablebuf,tablelength); 57 ftablein.close(); tablebuf[tablelength-1]=’{}0’; } } } MPI::COMM WORLD.Barrier(); if (size!=0){ MPI::COMM WORLD.Bcast(&graphlength,1,MPI::INT,0); if (graphlength==-10){ MPI::Finalize(); exit(1); } MPI::COMM WORLD.Bcast(&tablelength,1,MPI::INT,0); if (rank!=0) { graphbuf = new char[graphlength]; tablebuf = new char[tablelength]; } MPI::COMM WORLD.Bcast(graphbuf,graphlength,MPI::CHAR,0); MPI::COMM WORLD.Bcast(tablebuf,tablelength,MPI::CHAR,0); MPI::COMM WORLD.Barrier(); } graphoutname[6]=rank+’0’; fgraphout.open(graphoutname,ios::binary); fgraphout.write(graphbuf,graphlength); fgraphout.close(); tableoutname[6]=rank+’0’; ftableout.open(tableoutname,ios::binary); ftableout.write(tablebuf,tablelength); ftableout.close(); } //@ ˜MPIComm: destructor //———————————————– MPIComm::˜MPIComm() { MPI::Finalize(); } //@ FirstMsg(): espera al primer mensaje para este rank Event *MPIComm::FirstMsg() 58 { float msg[4]; int i; MPI::COMM WORLD.Recv(msg,4,MPI::FLOAT,MPI::ANY SOURCE,MPI::ANY TA Event *e1 = new Event((char)msg[0],msg[1],(int)msg[2],(int)msg[3]); return(e1); } Event * MPIComm::RecvMsg() { float *msg; msg = new float[4]; if(MPI::COMM WORLD.Iprobe(MPI::ANY SOURCE,MPI::ANY TAG)){ MPI::COMM WORLD.Recv(msg,4,MPI::FLOAT,MPI::ANY SOURCE,MPI::ANY if(msg[0]==(float)’F’){ ended++; return(NULL); } else{ Event *e1 = new Event((char)msg[0],msg[1],(int)msg[2],(int)msg[3]); return(e1); } } else return(NULL); } void MPIComm::SendMsg(Event *e, int target) { float msg[4]; msg[0]=(float)e->getName(); msg[1]=e->getTime(); msg[2]=(float)e->getTarget(); msg[3]=(float)e->getSource(); MPI::COMM WORLD.Send(msg,4,MPI::FLOAT,target,1); } //@ GetRank(): devuelve el identificador de procesador //———————————————– int MPIComm::getRank() 59 { return(rank); } //@ GetSize(): devuelve el nÃo mero de procesadores //———————————————– int MPIComm::getSize() { return(size); } //@ end(): Envia un mensaje a rank cero indicano que el // procesador ha terminado su simulacion //———————————————— void MPIComm::end(){ float msg[4]; msg[0]=(float)’F’; msg[1]=0; msg[2]=0; msg[3]=0; MPI::COMM WORLD.Send(msg,4,MPI::FLOAT,0,1); } //@ finished: Indica cuantos procesadores han terminado // su simulación //————————————————– int MPIComm::finished() { return (ended); } 14.9. // // // // Clase: LinkList / Archivo linklist.cc Archivo: linklist.cc implementacion de las clases parametrizadas ’LinkItem’ y ’LinkList’ R. Marcelin J. (21/06/99). (25/10/01) agregue los metodos “insertmarked()”. #if !defined( LINKLIST ) #define LINKLIST template <class T> class LinkItem; // item de una lista ligada template <class T> class LinkList; // lista ligada //class IntSet; template <class T> class LinkItem 60 { public: LinkItem(); // constructor por omision LinkItem(T & info); // constructor de tipos basicos LinkItem(T & info, float key); // constructor de clases compuestas ˜LinkItem(); // destructor LinkItem<T> *getPos(); // LinkItem<T> *getPre(); // T &getInfo(); // void setPos(LinkItem<T> * pos); // void setPre(LinkItem<T> * pre); // void setMark(); // void resetMark(); // int isMarked(); // friend class LinkList<T>; // acceso a los atrib. privados // friend class IntSet; protected: float key; // para establecer un orden T info; // campo de informacion int marked; // LinkItem<T> *pos; // apunta al vecino posterior LinkItem<T> *pre; // apunta al vecino previo }; template <class T> class LinkList // eficiente para ˜cientos de items { public: LinkList(); // constructor por omision ˜LinkList(); // destructor int getLength(); // devuelve la long. de la lista int isEmpty(); // verdadero si esta vacia void insert(LinkItem<T> *t); // agrega en orden segun la llave void insert(T & info, float key);// void insert(T & info); // void insertmarked(T & info, float key);// void insertmarked(T & info); // void remove(float key); // elimina al item que tiene la llave 61 void inqueue(LinkItem<T> *t); // agrega al final void inqueue(T & info, float key); // void inqueue(T & info); // /* void insertnoorder(LinkItem<T> *t); void insertnoorder(T & info); */ T dequeue(); // extrae al primero, usar c/isEmpty() T *find(float key); // busca un item por su llave T *next(); // devuelve el sig. item sin visitar // o 0, si ya acabo. T *next(float & key); // tambien devuelve su llave de orden // como efecto lateral T getNext(int pos); // devuelve el item que esta tantas posiciones adelante protected: LinkItem<T> *head; // Estos son los pivotes de la LinkItem<T> *tail; // lista. int length; }; //@ LinkItem(): constructor por omision //—————————————————————— template<class T> LinkItem<T>::LinkItem() { key = -1; pos = 0; pre = 0; marked = 0; // TRACE(lnk, -1, (“LinkItem constructor{}n”)); } //@ LinkItem(): constructor para tipos basicos //—————————————————————— template<class T> LinkItem<T>::LinkItem(T & info) { info = info; key = info; pos = 0; 62 pre = 0; marked = 0; // TRACE(lnk, -1, (“LinkItem constructor{}n”)); } //@ LinkItem(): constructor para clases //—————————————————————— template<class T> LinkItem<T>::LinkItem(T & info, float key): info( info) { key = key; pos = 0; pre = 0; marked = 0; // TRACE(lnk, -1, (“LinkItem constructor{}n”)); } //@ ˜LinkItem(): destructor //—————————————————————— template<class T> LinkItem<T>::˜LinkItem() { //TRACE(lnk, -1, (“LinkItem destructor{}n”)); } //@ getPos(): //—————————————————————— template<class T> LinkItem<T> *LinkItem<T>::getPos() { return pos; } //@ getPre(): //—————————————————————— template<class T> LinkItem<T> *LinkItem<T>::getPre() { return pre; } //@ getInfo(): //—————————————————————— template<class T> T &LinkItem<T>::getInfo() { return info; } //@ setPos(): //—————————————————————— template<class T> void LinkItem<T>::setPos(LinkItem<T> * pos) 63 { pos = pos; } //@ setPre(): //—————————————————————— template<class T> void LinkItem<T>::setPre(LinkItem<T> * pre) { pre = pre; } //@ setMark(): //—————————————————————— template<class T> void LinkItem<T>::setMark() { marked = 1; } //@ resetMark(): //—————————————————————— template<class T> void LinkItem<T>::resetMark() { marked = 0; } //@ isMarked(): //—————————————————————— template<class T> int LinkItem<T>::isMarked() { return marked; //@ LinkList(): constructor por omision //—————————————————————— template<class T> LinkList<T>::LinkList() { head = new LinkItem<T>(); tail = new LinkItem<T>(); head->pos = tail; tail->pre = head; length = 0; // TRACE(lnk, -1, (“LinkList constructor{}n”)); } //@ ˜LinkList(): destructor //—————————————————————— template<class T> LinkList<T>::˜LinkList() { // TRACE(lnk, -1, (“LinkList destructor{}n”)); //@ getLength(): 64 } } //—————————————————————— template<class T> int LinkList<T>::getLength() { return length; //@ getNext(): //—————————————————————— template<class T> T LinkList<T>::getNext(int posit) { LinkItem<T> *aux; int i; aux=head->pos; for(i=1;i<posit;i++){ if(aux==tail){ return(-1); } aux=aux->pos; } return(aux->info); } //@ isEmpty(): //—————————————————————— template<class T> int LinkList<T>::isEmpty() { return (head->pos == tail); } } // @ insert(): agrega en orden segun la llave ’t.key’ //—————————————————————— template<class T> void LinkList<T>::insert(LinkItem<T> *t) { LinkItem<T> *h1, *h2; h2 = head; h1 = h2->pos; while ((h1!=tail)&&(t->key >= h1->key)){ h2 = h1; h1 = h1->pos; }; h2->pos = t; t->pre = h2; h1->pre = t; t->pos = h1; length++; } // @ insert(); 65 //—————————————————————— template<class T> void LinkList<T>::insert(T & info, float key) { LinkItem<T> *h1, *h2, *t; t = new LinkItem<T>( info, key); insert(t); } //@ insert(): //—————————————————————— template<class T> void LinkList<T>::insert(T & info) { LinkItem<T> *h1, *h2, *t; t = new LinkItem<T>( info); insert(t); } // @ insertmarked(): //—————————————————————— template<class T> void LinkList<T>::insertmarked(T & info, float key) { LinkItem<T> *h1, *h2, *t; t = new LinkItem<T>( info, key); t->setMark(); insert(t); } //@ insertmarked(): //—————————————————————— template<class T> void LinkList<T>::insertmarked(T & info) { LinkItem<T> *h1, *h2, *t; t = new LinkItem<T>( info); t->setMark(); insert(t); } //@ remove(): elimina al item con la llave indicada //—————————————————————— template<class T> void LinkList<T>::remove(float key) 66 { LinkItem<T> *h1, *h2, *h3; h2 = head; h1 = h2->pos; while ((h1!=tail)&&(h1->key!= key)) { h2 = h1; h1 = h1->pos; }; if (h1!=tail) { h3 = h1; h1 = h1->pos; h2->pos = h1; h1->pre = h2; delete h3; length–; }; } //@ inqueue(): agrega al final //—————————————————————— template<class T> void LinkList<T>::inqueue(LinkItem<T> *t) { LinkItem<T> *h1, *h2; h1 = tail; h2 = h1->pre; h2->pos = t; t->pre = h2; h1->pre = t; t->pos = h1; length++; } //@ inqueue(): //—————————————————————— template<class T> void LinkList<T>::inqueue(T & info, float key) { LinkItem<T> *t; t = new LinkItem<T>( info, key); inqueue(t); } //@ inqueue(): 67 //—————————————————————— template<class T> void LinkList<T>::inqueue(T & info) { LinkItem<T> *t; t = new LinkItem<T>( info); inqueue(t); } //@ dequeue(): //—————————————————————— template<class T> T LinkList<T>::dequeue() { LinkItem<T> *h1, *h2; T temp; h1 = head->pos; h2 = h1->pos; head->pos = h2; h2->pre = head; h1->pos = h1->pre = 0; temp = h1->info; delete h1; length–; return temp; } //@ find(): devuelve al item que contiene la llave, 0 si falla //—————————————————————— template<class T> T *LinkList<T>::find(float key) { LinkItem<T> *h1, *h2; h2 = head; h1 = h2->pos; while ((h1!=tail)&&(h1->key!= key)) { h2 = h1; h1 = h1->pos; }; if (h1!=tail) return &(h1->info); else return 0; } 68 //@ next(): devuelve el siguiente item sin visitar, 0 si acabo //—————————————————————— template<class T> T *LinkList<T>::next() { LinkItem<T> *h1, *h2; T temp; h2 = head; h1 = h2->pos; while ((h1!=tail)&&(h1->isMarked())) { h2 = h1; h1 = h1->pos; }; if (h1!=tail){ h1->setMark(); return &(h1->info); } else { h2 = head; h1 = h2->pos; while (h1!=tail){ h1->resetMark(); h2 = h1; h1 = h1->pos; }; return 0; }; } //@ next(): tambien devuelve su llave de orden, como efecto lateral. //—————————————————————— template<class T> T *LinkList<T>::next(float & key) { LinkItem<T> *h1, *h2; T temp; h2 = head; h1 = h2->pos; while ((h1!=tail)&&(h1->isMarked())){ h2 = h1; h1 = h1->pos; }; if (h1!=tail){ 69 h1->setMark(); key = h1->key; return &(h1->info); } else { h2 = head; h1 = h2->pos; while (h1!=tail){ h1->resetMark(); h2 = h1; h1 = h1->pos; }; return 0; }; } #endif 14.10. Clase: AVLTree / Archivo: avltree.cc // Archivo: avltree.cc // implementacion de las clases parametrizadas ’AVLItem’ y ’AVLTree’ // R. Marcelin J. (21/06/99) #if !defined ( AVLTREE ) #define AVLTREE //#include “trace.h” // para rastrear operaciones //DEFINE TRACE MODULE(avl, “avl”); template <class T> class AVLItem; // item de un arbol balanceado template <class T> class AVLTree; // Adelson, Velski y Landis template <class T> class AVLItem { public: AVLItem(); // constructor AVLItem(T & info); // constructor AVLItem(T & info, float key); // constructor ˜AVLItem(); // destructor private: 70 float key; // para establecer un orden T info; // campo de informacion int bal; // altura izq - altura der : -1,0,1 AVLItem *pre; // vecino izquierdo AVLItem *pos; // vecino derecho friend class AVLTree<T>; // acceso a los atrib. privados }; // se usa en AVLTree enum search { FAIL = 0 , // no lo encontro EUREKA = 1 , // lo encontro en un hijo LFOUND = 2 , // es descendiente de un hijo izq. RFOUND = 3 }; // es descendiente de un hijo der. template <class T> class AVLTree // eficiente para ˜miles de items { public: // constructor AVLTree(); // Destructor ˜AVLTree(); // total de items insertados int getSize(); // verdadero si el arbol esta vacio int isEmpty(); // descenso recursivo en preorden, desde la raiz void visit(AVLItem<T> *t, int nivel); void visit(); // balancea arbol podado por la izquierda AVLItem<T> *lbalance(AVLItem<T> *t); // balancea arbol podado por la derecha AVLItem<T> *rbalance(AVLItem<T> *t); // incrementa el arbol y verifica su balance void insert(T & info); void insert(T & info, float key); void insert(AVLItem<T> *n); 71 AVLItem<T> *insert(AVLItem<T> *n, AVLItem<T> *t); // devuelve el item con la menor clave de todo el arbol // usar con isEmpty() T minval(); T minval(AVLItem<T> *t, enum search &result); // busca al item con la mayor clave de un subarbol AVLItem<T> *maxval(AVLItem<T> *t, T & info, enum search &result); // elimina un item particular, segun su llave int remove(float key); enum search remove(float key, AVLItem<T> *t); // devuelve ap. a item que busca por su llave, 0 en otro caso T *find(float key); T *find(float key, AVLItem<T> *t); private: AVLItem<T> *root; // ra¡z int size; // taman˜o del arbol int balance; // 1 si debe verificarse el balance }; //@ AVLItem(): constructor por omision //—————————————————————— template<class T> AVLItem<T>::AVLItem() { key = -1; pos = 0; pre = 0; bal = 0; // TRACE(avl, -1, (“AVLItem constructor{}n”)); } //@ AVLItem(): constructor para tipos basicos //—————————————————————— template<class T> AVLItem<T>::AVLItem(T & info) { info = info; key = info; 72 pos = 0; pre = 0; bal = 0; // TRACE(avl, -1, (“AVLItem constructor{}n”)); } //@ AVLItem(): constructor para clases // es mejor inicializar por fuera un miembro compuesto //—————————————————————— template<class T> AVLItem<T>::AVLItem(T & info, float key) : info( info) { key = key; pos = 0; pre = 0; bal = 0; // TRACE(avl, -1, (“AVLItem constructor{}n”)); } //@ ˜AVLItem(): destructor // —————————————————————– template <class T> AVLItem<T>::˜AVLItem() { // TRACE(avl, -1, (“AVLItem destructor{}n”)); // AVLTree(): Constructor // —————————————————————– template<class T> AVLTree<T>::AVLTree() { root = 0; size = 0; balance = 0; // TRACE(avl, -1, (“AVLTree constructor{}n”)); } // AVLTree(): Destructor // —————————————————————– template<class T> AVLTree<T>::˜AVLTree() { // TRACE(avl, -1, (“AVLTree destructor{}n”)); 73 } } // getSize(): total de items insertados // —————————————————————– template<class T> int AVLTree<T>::getSize() { return size; // isEmpty(): verdadero si el arbol esta vacio // —————————————————————– template<class T> int AVLTree<T>::isEmpty() { return (root == 0); // visit(): comienza por la ra¡z // —————————————————————– template<class T> void AVLTree<T>::visit() { visit(root, 0); } } } // visit(): descenso rec. en preorden, imprime contenido y profundidad // —————————————————————– template<class T> void AVLTree<T>::visit(AVLItem<T> *t, int nivel) { if (t != 0) { visit(t->pre, nivel+1); cout << t->key << “:” << nivel << “:” << t->bal << “{}n”; visit(t->pos, nivel+1); }; } // lbalance(): balancea un arbol podado por la izquierda // —————————————————————– template<class T> AVLItem<T> *AVLTree<T>::lbalance(AVLItem<T> *t) { AVLItem<T> *t1,*t2, *t3; int b2, b3; t1 = t; switch (t1->bal){ case -1: t1->bal = 0; break; case 0: t1->bal = 1; balance = 0; 74 break; case 1: t2 = t1->pos; b2 = t2->bal; if (b2 >= 0) // rotación simple RR { t1->pos = t2->pre; t2->pre = t1; if (b2 == 0){ t1->bal = 1; t2->bal = -1; balance = 0; } else { t1->bal = 0; t2->bal = 0; }; t1 = t2; } else // rotación doble RL { t3 = t2->pre; b3 = t3->bal; t2->pre = t3->pos; t3->pos = t2; t1->pos = t3->pre; t3->pre = t1; t1->bal = (b3 == 1)? -1: 0; t2->bal = (b3 == -1)? 1: 0; t3->bal = 0; t1 = t3; }; }; return t1; }; // rbalance(): balancea un arbol podado por la derecha // —————————————————————– 75 template<class T> AVLItem<T> *AVLTree<T>::rbalance(AVLItem<T> *t) { AVLItem<T> *t1,*t2, *t3; int b2, b3; t1 = t; switch (t1->bal) { case 1: t1->bal = 0; break; case 0: t1->bal = -1; balance = 0; break; case -1: t2 = t1->pre; b2 = t2->bal; if (b2 <= 0) // rotación simple LL { t1->pre = t2->pos; t2->pos = t1; if (b2 == 0) { t1->bal = -1; t2->bal = 1; balance = 0; } else{ t1->bal = 0; t2->bal = 0; }; t1 = t2; } else // rotación doble LR { t3 = t2->pos; b3 = t3->bal; t2->pos = t3->pre; 76 t3->pre = t2; t1->pre = t3->pos; t3->pos = t1; t1->bal = (b3 == -1)? 1: 0; t2->bal = (b3 == 1)? -1: 0; t3->bal = 0; t1 = t3; }; }; return t1; }; // insert(): comienza exploracion por la ra¡z // —————————————————————– template<class T> void AVLTree<T>::insert(T & info) { AVLItem<T> *n; n = new AVLItem<T>( info); root = insert(n, root); } // insert(): comienza exploracion por la ra¡z // —————————————————————– template<class T> void AVLTree<T>::insert(T & info, float key) { AVLItem<T> *n; n = new AVLItem<T>( info, key); root = insert(n, root); } // insert(): comienza exploracion por la ra¡z // —————————————————————– template<class T> void AVLTree<T>::insert(AVLItem<T> *n) { root = insert(n, root); } // insert(): devuelve ap. al subarbol incrementado y rebalanceado. // —————————————————————– template<class T> AVLItem<T> *AVLTree<T>::insert(AVLItem<T> *n, AVLItem<T> *t) { AVLItem<T> *t1, *t2, *t3; 77 t1 = t; if (t1 == 0) { t1 = n; balance = 1; size++; // primera vez } else if (n->key < t1->key) // sin el = { t1->pre = insert(n, t1->pre); // desciende por la izquierda if (balance) { switch(t1->bal){ case 1: t1->bal = 0; balance = 0; break; case 0: t1->bal = -1; break; case -1: t2 = t1->pre; if (t2->bal == -1) // rot. simple LL { t1->pre = t2->pos; t2->pos = t1; t1->bal = 0; t1 = t2; } else{ t3 = t2->pos; // rot. doble LR t2->pos = t3->pre; t3->pre = t2; t1->pre = t3->pos; t3->pos = t1; t1->bal = (t3->bal == -1)? 1: 0; t2->bal = (t3->bal == 1)? -1: 0; t1 = t3; }; t1->bal = 0; balance = 0; }; }; } else { 78 t1->pos = insert(n, t1->pos); // desciende por la derecha if (balance){ switch(t1->bal) { case -1: t1->bal = 0; balance = 0; break; case 0: t1->bal = 1; break; case 1: t2 = t1->pos; if (t2->bal == 1) // rot. simple RR { t1->pos = t2->pre; t2->pre = t1; t1->bal = 0; t1 = t2; } else { t3 = t2->pre; // rot. doble RL t2->pre = t3->pos; t3->pos = t2; t1->pos = t3->pre; t3->pre = t1; t1->bal = (t3->bal == 1)? -1: 0; t2->bal = (t3->bal == -1)? 1: 0; t1 = t3; }; t1->bal = 0; balance = 0; }; }; }; return t1; }; // minval(): devuelve el item con la menor llave de todo el arbol 79 // —————————————————————– template<class T> T AVLTree<T>::minval() { enum search result = FAIL; T info; AVLItem<T> *t; if (root->pre == 0) { t = root; info = root->info; root = root->pos; result = EUREKA; delete t; } else info = minval(root, result); if (result != FAIL) size–; return info; }; // minval(): descenso rec. por el lado izquierdo // —————————————————————– template<class T> T AVLTree<T>::minval(AVLItem<T> *t, enum search &result) { AVLItem<T> *t1, *t2; T info; t1 = t; if (t1->pre == 0) { info = t->info; result = EUREKA; } else{ info = minval(t1->pre, result); if (result == LFOUND) { if (balance) t1->pre = lbalance(t1->pre); } else if (result == EUREKA) { t2 = t1->pre; // procede a podar a su hijo menor 80 t1->pre = t2->pos; // y lo borra result = LFOUND; delete t2; balance = 1; }; }; return info; }; // maxval(): busca al item con la mayor clave de un subarbol // —————————————————————– template<class T> AVLItem<T> *AVLTree<T>::maxval(AVLItem<T> *t, T & info, enum search &result) { AVLItem<T> *t1, *t2; t1 = t; if (t1->pos == 0) { info = t1->info; // procede a podar a su hijo mayor t2 = t1; t1 = t1->pre; result = EUREKA; delete t2; balance = 1; } else{ t1->pos = maxval(t1->pos, info, result); if (balance) t1 = rbalance(t1); result = RFOUND; }; return t1; }; //@ remove(): inicia la busqueda por la raiz //—————————————————————— template<class T> int AVLTree<T>::remove(float key) { enum search result = FAIL; T info; AVLItem<T> *d; result = remove( key, root); 81 if (result == EUREKA){ d = root; if (root->pos == 0) { root = root->pre; delete d; } else if (root->pre == 0) { root = root->pos; delete d; } else { root->pre = maxval(root->pre, info, result); root->setInfo( info); if (balance) root = lbalance(root); }; } else if (result == LFOUND) { if (balance) root = lbalance(root); } else if (result == RFOUND) { if (balance) root = rbalance(root); }; if (result != FAIL) size–; return result; }; //@ remove(): elimina a un item segun su llave y verifica el balance //—————————————————————— template<class T> enum search AVLTree<T>::remove(float key, AVLItem<T> *t) { enum search result; AVLItem<T> *t1, *t2; T info; t1 = t; if (t1 == 0){ result = FAIL; // no existe } 82 else if ( key == t1->key) { result = EUREKA; // “-lo encontre!” } else if ( key < t1->key) { result = remove( key, t1->pre); // desciende por la izq. if (result == RFOUND){ if (balance) t1->pre = rbalance(t1->pre); } else if (result == LFOUND){ if (balance) t1->pre = lbalance(t1->pre); } else if (result == EUREKA) { t2 = t1->pre; if (t2->pos == 0) { t1->pre = t2->pre; delete t2; balance = 1; } else if (t2->pre == 0) { t1->pre = t2->pos; delete t2; balance = 1; } else // el mayor de sus menores { t2->pre=maxval(t2->pre, info, result); t2->setInfo( info); if (balance) t1->pre = lbalance(t1->pre); }; }; result = LFOUND; } else{ result = remove( key, t1->pos); // desciende por la der. if (result == RFOUND){ if (balance) t1->pos = rbalance(t1->pos); } else if (result == LFOUND) { 83 if (balance) t1->pos = lbalance(t1->pos); } else if (result == EUREKA) { t2 = t1->pos; if (t2->pos == 0) { t1->pos = t2->pre; delete t2; balance = 1; } else if (t2->pre == 0){ t1->pos = t2->pos; delete t2; balance = 1; } else // el mayor de sus menores { t2->pre=maxval(t2->pre, info, result); t2->setInfo( info); if (balance) t1->pos = lbalance(t1->pos); }; }; result = RFOUND; }; return result; }; // find(): // —————————————————————– template<class T> T *AVLTree<T>::find(float key) { return find( key, root); } // find(): devuelve ap. a item que busca por su llave, 0 en otro caso // —————————————————————– template<class T> T *AVLTree<T>::find(float key, AVLItem<T> *t) { T *info; if (t == 0) info = 0; // no lo encontro else if ( key == t->key) 84 info = &(t->info); // aqui esta else if ( key < t->key) info = find( key, t->pre); // desciende por la izquierda else info = find( key, t->pos); // desciende por la derecha return info; }; #endif 15. Bibliografı́a Robbins, Kay A & Robbins, Steven. “UNIX, Programación Prática”. Ed. Prentice Hall Hispanoamericana, México, 1997. Liberty Jesse, “C++ para principiantes”. Ed. Pearson Education, México, 1999. http://www.mpi-forum.org MPI: http://www.millennium.berkeley.edu/docs/mpi/ PVM: http://www.csm.ornl.gov/pvm/ MPICH: http://www.mcs.anl.gov/mpi/mpich/ LAM/MPI: http://www.mpi.nd.edu/lam/ JPVM (pvm para java): http://www.isye.gatech.edu/chmsr/jPVM/ MPICH para Windows NT: http://www.jics.utk.edu/mpint/WINMPICH.txt Parallel computing (517 of the best sites selected by humans): http://www.cbel.com/Parallel computing/?order=alpha Ricardo Marcelin, Sergio Rajsbaum. Curso de principios de computación distribuida: http://www.cs.auckland.ac.nz/ ute/220ft/graphalg/node10.html 85 Algoritmo DFS: http://ww5.java3.datastructures.net:8081/handouts/DFS.pdf http://infosun.fmi.uni-passau.de/GTL/manual/a00103.html 86