Vous êtes sur la page 1sur 157

Programacin Avanzada, Concurrente y Distribuida

Diego RodrguezLosada Gonzlez Pablo San Segundo Carrillo

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

PRLOGO

PARTE I. Desarrollo de una aplicacin distribuida y concurrente en LINUX


1. EDICIN, COMPILACIN, Y DEPURACIN DE UNA APLICACIN C/C++ BAJO LINUX 1.1. 1.2. 1.3. 1.4. 1.5. 1.6. 1.7. 1.8. 1.9. 1.10. 1.11. 1.12. 1.13. 1.14. 1.15. INTRODUCCIN LOGIN EN MODO TEXTO MANEJO DE ARCHIVOS Y DIRECTORIOS EN MODO TEXTO. EL EDITOR DE TEXTO DESARROLLO C/C++ EN LINUX EN MODO TEXTO EL PROCESO DE CREACIN DE UN EJECUTABLE LAS HERRAMIENTAS DE DESARROLLO EL COMPILADOR GCC MAKEFILE Y LA HERRAMIENTA MAKE TIPOS DE ERROR DEPURACIN DE LA APLICACIN. CREACIN DE UN SCRIPT DESARROLLO EN UN ENTORNO GRAFICO EJERCICIO PRCTICO EJERCICIO PROPUESTO 11 11 12 12 15 15 16 17 17 19 20 21 22 23 23 25 27 27 28 29 30 32 35 36 38 42 44 44 45 45

2. INTRODUCCIN A LOS SISTEMAS DISTRIBUIDOS. COMUNICACIN POR SOCKETS 2.1. 2.2. 2.3. 2.3.1 2.3.2 2.4. 2.4.1 2.4.2 2.5. 2.6. 2.6.1 2.6.2 2.7. OBJETIVOS SISTEMA DISTRIBUIDO SERVICIOS DE SOCKETS EN POSIX PROGRAMA CLIENTE SERVIDOR ENCAPSULACIN DE UN SOCKET EN UNA CLASE C++ ENVO DE MLTIPLES MENSAJES CONEXIONES MLTIPLES. ESTRUCTURA DE FICHEROS TRANSMITIENDO EL PARTIDO DE TENIS CONEXIN ENVO DE DATOS EJERCICIOS PROPUESTOS

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


3. COMUNICACIONES Y CONCURRENCIA 3.1. 3.2. 3.3. 3.3.1 3.4. 3.5. 3.6. 3.7. 3.8. 3.9. 3.10. INTRODUCCIN REQUISITOS FUNCIONAMIENTO DE GLUT LANZANDO UN HILO ESTRUCTURA DEL SERVIDOR MLTIPLES CONEXIONES SIMULTANEAS MOSTRAR LOS CLIENTES CONECTADOS RECEPCIN COMANDOS MOVIMIENTO GESTIN DESCONEXIONES FINALIZACIN DEL PROGRAMA EJERCICIO PROPUESTO

4
47 47 49 49 50 51 52 53 55 56 56 57 59 59 60 61 62 64 69

4. COMUNICACIN Y SINCRONIZACIN INTERPROCESO 4.1. 4.2. 4.3. 4.4. 4.5. 4.6. INTRODUCCIN EL PROBLEMA DE LA SINCRONIZACION COMUNICACIN INTERPROCESO TUBERAS CON NOMBRE MEMORIA COMPARTIDA EJERCICIOS PROPUESTOS

PARTE II. Programacin avanzada


5. PROGRAMACIN DE CDIGO EFICIENTE 5.1. 5.2. 5.3. 5.4. 5.5. 5.5.1 5.5.2 5.5.3 5.5.4 5.5.5 5.6. 5.6.1 5.6.2 5.6.3 5.7. 5.8. INTRODUCCIN MODOS DE DESARROLLO TIPOS DE OPTIMIZACIONES VELOCIDAD DE EJECUCIN ALGUNAS TCNICAS CASOS FRECUENTES BUCLES GESTIN DE MEMORIA TIPOS DE DATOS TCNICAS EN C++ CASOS PRCTICOS ALGORTMICA VS. MATEMTICAS GENERACIN DE NMEROS PRIMOS PRECOMPUTACIN DE DATOS OBTENIENDO PERFILES (PROFILING) DEL CDIGO CONCLUSIONES 73 73 77 77 78 79 79 80 83 85 86 87 87 88 90 93 95

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


6. SERIALIZACIN DE DATOS 6.1. 6.2. 6.3. 6.3.1 6.3.2 6.4. 6.4.1 6.4.2 6.5. INTRODUCCIN REPRESENTACIN OBJETOS EN MEMORIA SERIALIZACIN EN C CON FORMATO (TEXTO) SIN FORMATO (BINARIA) SERIALIZACIN EN C++ CON FORMATO (TEXTO) SIN FORMATO (BINARIA) CONCLUSIONES

5
97 97 102 103 104 104 107 108 111 112 113

7. BSQUEDAS EN UN ESPACIO DE ESTADOS MEDIANTE RECURSIVIDAD 7.1. 7.2. 7.2.1 7.2.2 7.2.3 7.3. 7.4.

INTRODUCCIN 113 115 BSQUEDA PRIMERO EN PROFUNDIDAD TERMINOLOGA 116 116 ESTRUCTURAS DE DATOS ANLISIS 117 119 BSQUEDA PRIMERO EN ANCHURA METODOLOGA GENERAL DE RESOLUCIN DE UN PROBLEMA DE BSQUEDA MEDIANTE COMPUTACIN 120 7.5. IMPLEMENTACIN DE UNA BSQUEDA DFS MEDIANTE RECURRENCIA 121 122 7.5.1 LA PILA DE LLAMADAS 124 7.5.2 BSQUEDA DFS COMO RECURSIN 8. EJECUCIN DISTRIBUIDA DE TAREAS 133 133 134 134 135 136 138 139 140 141 145 147 147 148 148 153 153

8.1. 8.2. 8.2.1 8.2.2 8.2.3 8.3. 8.3.1 8.3.2 8.3.3 8.3.4 8.4. 8.4.1 8.4.2 8.4.3 8.5. 8.5.1

INTRODUCCIN EL PROBLEMA DE LAS NREINAS HISTORIA CARACTERSTICAS 2ESTRUCTURAS DE DATOS IMPLEMENTACIN CENTRALIZADA DESCRIPCIN ESTRUCTURAS DE DATOS CONTROL DE LA BSQUEDA ALGORITMO DE BSQUEDA IMPLEMENTACIN DISTRIBUIDA ARQUITECTURA CLIENTESERVIDOR PROTOCOLO DE COMUNICACIN IMPLEMENTACIN DEL CLIENTE IMPLEMENTACIN DEL SERVIDOR COMUNICACIN CON EL CLIENTE

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

PRLOGO
Generalmente la formacin en informtica de un ingeniero (industrial, automtica, telecomunicaciones o similar) comienza por la programacin estructurada, en lenguajes como C o Matlab, y luego se complementa con Programacin Orientada a Objetos (POO) e Ingeniera del Software, con Anlisis y Diseo Orientados a Objetos, UML, etc. Sin embargo, existen una serie de tcnicas y tecnologas software que escapan del alcance de los anteriores cursos. La programacin de tareas concurrentes, los sistemas distribuidos, la programacin de cdigo eficiente o algortmica avanzada son temas que quedan a menudo relegados, y sin embargo son muy necesarios en tareas de ingeniera industrial, comunicaciones y similares. Este libro trata de cubrir dichos aspectos, de una manera prctica y aplicada. La primera parte desarrolla una aplicacin grfica distribuida: un tpico juego de computador en red. En esta aplicacin se requiere el uso de comunicaciones por red (con sockets), as como la utilizacin de tcnicas de programacin concurrente con multiproceso y multihilo, de una manera que esperamos que sea atractiva y motivadora para el lector. El desarrollo se realiza en Linux (Posix), presentando una introduccin al manejo bsico, desarrollo y depuracin con herramientas GNU como g++, make y gdb. El cdigo de soporte para estos captulos se encuentra en www.elai.upm.es La segunda parte cubre algunos tpicos genricos avanzados como la programacin de cdigo eficiente, la serializacin de datos, la recurrencia o la computacin distribuida, tpicos que muchas veces estn ntimamente relacionados con los anteriores.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

Parte I. Desarrollo de una aplicacin distribuida y concurrente en LINUX

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

11

EDICIN, COMPILACIN, Y DEPURACIN DE UNA APLICACIN C/C++


BAJO LINUX
1.1. INTRODUCCIN
En este primer tema realizamos una aproximacin al SO operativo linux, y fundamentalmente al desarrollo de aplicaciones en C/C++, desarrolladas, depuradas y ejecutadas en un computador con Linux. Aunque el objetivo de este curso es el aprendizaje de programacin concurrente y sistemas distribuidos, en este primer tema nos ceiremos al trabajo de desarrollo convencional en linux, para aprender tanto el desarrollo sin interfaz grafica de ventanas, como algunas de las herramientas graficas. Tambin se manejaran algunos comandos o mandatos bsicos de linux para crear, editar y manejar archivos, y se introducir el uso de las herramientas de desarrollo bsico como son gcc, g++, make y gdb. Este tema comienza por la descripcin de los comandos bsicos para trabajar en modo texto, para despus desarrollar y depurar una pequea aplicacin ejemplo en modo texto. Por ultimo, se trabajara en modo grafico, completando un cdigo ya avanzado para terminar con el juego del tenis que funcione en modo local, para dos jugadores, esto es, los dos jugadores utilizan el mismo teclado y la misma pantalla.

1.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

12

Figura 11. Objetivo del captulo: Desarrollo del juego del Tenis en modo local

1.2. LOGIN EN MODO TEXTO


Aunque el computador arranque en modo grafico, la primera parte de esta prctica se va a desarrollar en modo texto. Para ello cambiese del terminal grafico al primer terminal de texto, mediante la correspondiente combinacin de teclas (Ctrl+Alt+F1) Entrar en la cuenta de usuario correspondiente. Consejo: Aunque dispongas de la contrasea de administrador es absolutamente recomendable no utilizarla para trabajar normalmente. En caso de que seas el administrador del sistema, crea una cuenta de usuario normal para realizar la practica. Probar a realizar el login en los distintos terminales virtuales (saliendo luego con el comando exit de los que no se vayan a utilizar)

1.3. MANEJO DE ARCHIVOS Y DIRECTORIOS EN MODO TEXTO.


Para familiarizarse con el manejo de archivos y directorios en linux se va a crear la siguiente estructura de archivos, en la que los archivos de texto contienen el texto Hola que tal: /home/usuario/ |>carpeta1 | | | |>subcarpeta11 | |>archivo11.txt |>archivo1.txt

|>carpeta2 |>archivo2.txt Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida Utilizar y explorar los comandos y opciones siguientes: Tabla 11. Comandos bsicos consola linux Comando pwd ls Accin muestra el directorio actual muestra el contenido del directorio actual crea el directorio con el nombre dado cambia al directorio que indica la ruta correspondiente concatena el fichero a salida estndar Opciones

13

-a (muestra todos los archivos, incluidos ocultos) l, muestra detalles de los archivos

mkdir [directorio] cd [ruta] cat [fichero]

- significa entrada estndar. Para crear un archivo se puede redireccionarla de la siguiente forma cat >nombre_fichero.txt

chmod usuario+permiso [fichero] rm [archivo]

cambia los permisos (r=read, w=write, x=execute) a usuario (a=all, o=others, u=user, g=group) Borra el archivo

-r = borra recursivamente el directorio seleccionado (OJO, usar con mucha precaucin)

cp [origen] [destino] mv [origen] [destino] rmdir [directorio] exit o logout

Copia el archivo o archivos origen al destino seleccionado Mueve el archivo o archivos origen al destino seleccionado Borra el directorio, que previamente debe estar vaco Termina la sesin (salir)

Tambin sirve para renombrar un archivo

Tabla 12. Caracteres comodin (wildcars)

* ?

Una cadena de caracteres cualesquiera Un carcter cualquiera

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida Tabla 13. Directorios importantes . .. cd Directorio actual Directorio superior Vuelve al directorio inicial raiz del usuario \home\usuario Tabla 14. Ayuda Comando man [comando] comando info [comando] whatis [comando] funcin Muestra las paginas man del comando seleccionado Muestra una ayuda breve del comando al que se aplica Muestra las paginas info del comando al que se aplica Busca en una base de datos descripciones cortas del comando Opcion --help -h Opcion

14

Tabla 15. Ayudas del shell bash Teclas Tab funcin Autocompletar, rellena el nombre del comando o archivo segn las posibles opciones que conozca Muestra todas las opciones que tiene autocompletar Sube en la historia de comandos Baja en la historia de comandos Opcion

Tab+Tab Arrow Up Arrow Down

Una vez creada la estructura, quitar el permiso de escritura al archivo11.txt e intentar concatenarle la cadena Muy bien gracias. Volver a reinstaurar el permiso y repetir la operacin. Borrar primero el archivo2.txt y luego la carpeta2. Borrar a continuacin todo el arbol de la carpeta1.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

15

1.4. EL EDITOR DE TEXTO


Se va a utilizar el editor vi o vim para crear y modificar los archivos de cdigo fuente necesarios, por ser el editor incluido por defecto en linux, y del que conviene tener al menos unas nociones bsicas que nos permitan sacarnos de un apuro en caso de necesidad. Para crear un archivo nuevo en la carpeta actual teclear: vi [fichero] Si el archivo no existe lo crea y si existe lo abre para editar. vi tiene dos modos de funcionamiento: Modo comando: cada tecla realiza una funcin especfica (borrar, mover) Este es el modo por defecto al arrancar el editor. Modo insercin: cada tecla inserta el carcter correspondiente en el texto. Para entrar en este modo se debe pulsar la tecla i y para salir de el se debe pulsar Esc. :w graba el archivo al disco :q salir de editor :q! salir del editor sin grabar los cambios (forzar la salida) :wq grabar y salir

Operaciones bsicas

1.5. DESARROLLO C/C++ EN LINUX EN MODO TEXTO


Vamos a construir una aplicacin con dos ficheros fuente, que muestre por pantalla una tabla de senos de varios ngulos. Para ello seguiremos los siguientes pasos: 1. Verificar mediante pwd que se encuentra en el directorio de usuario adecuado 2. Crear una carpeta pract1 que va a contener los archivos de la practica, y cambiar el directorio actual a la misma 3. Crear los archivos fuente siguientes:

/* * */

archivo: principal.c

#include <stdio.h> #include misfunc.h

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


int main(void) { int i; for(i=0;i<10;i++) { printf("Seno de %d es %f \n",i,seno(i)); } return 1; }

16

/* * archivo: misfunc.h */ #ifndef _MIS_FUNC_H_INCLUDED #define _MIS_FUNC_H_INCLUDED float seno(float num); #endif //_MIS_FUNC_H_INCLUDED

/* * */

archivo: misfunc.c

#include misfunc.h #include <math.h> float seno(float num) { return sin(num); }

1.6. EL PROCESO DE CREACIN DE UN EJECUTABLE


El compilador genera un fichero o modulo objeto (binario) por cada uno de los ficheros fuentes contenidos en el proyecto. Estos mdulos objeto no necesitan para ser compilados ms que el fichero fuente de origen, aunque se referencien funciones externas a dicho fichero. El proceso de enlazado une los mdulos objeto resolviendo las referencias entre ellos, as como las referencias a posibles bibliotecas o libreras externas al proyecto, y generando el archivo ejecutable. El sistema operativo es el encargado de unir el ejecutable con las libreras dinmicas cuando el programa es cargado en memoria.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida Fichero fuente A .c, .cpp Fichero fuente B .c, .cpp

17

COMPILADOR Biblioteca esttica A .a Biblioteca esttica B .a

Modulo objeto A .o

Modulo objeto B .o

LINKADO

Ejecutable

Libreras dinmicas .so

EJECUCION

Proceso en ejecucin Figura 12. Proceso de creacin de un ejecutable

1.7. LAS HERRAMIENTAS DE DESARROLLO


Se van a utilizar a partir de ahora los compiladores y distintas herramientas. Puede ser que en su sistema linux no vengan instaladas por defecto. Si ese es el caso, debe de instalarlas. El gestor de aplicaciones o paquetes de su distribucin le ayudara a hacerlo. En cualquier caso es importante remarcar que las herramientas de desarrollo utilizadas son GNU, con licencia GPL, es decir son gratuitas y su instalacin es totalmente legal. Si utiliza un sistema basado en Debian, la forma ms sencilla de instalar estas herramientas seria: sudo apt-get install build-essential

1.8. EL COMPILADOR GCC


El compilador utilizado en linux se llama gcc. La sintaxis adecuada para la compilacin y linkado del anterior programa seria: gcc o prueba principal.c misfunc.c lm Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida Ejecutar el programa mediante: ./prueba

18

Figura 13. Salida por pantalla de nuestra aplicacin Comprobar con ls al los permisos de ejecucin del archivo ls -al La sintaxis es la siguiente: gcc o [nombre_ejecutable] [ficheros_fuente] l[librera] Realmente este comando ha realizado la compilacin y el linkado todo seguido, de forma transparente para el usuario. Si se desea desacoplar las dos fases se realiza de la siguiente manera: Compilacin fichero a fichero : gcc c principal.c gcc c misfunc.c (Ntese que aqu no es necesario especificar que se va a linkar con la librera matemtica, ya que solo se esta compilando en un modulo objeto .o) Compilacin de varios ficheros en la misma lnea gcc c principal.c misfunc.c Enlazado

gcc o prueba principal.o misfunc.o lm Ntese que la opcin lm hace referencia a linkar l con la librera m o de nombre completo libm.a o libm.so que es la librera estndar matemtica en sus versiones estticas o dinmicas. Buscar con find / name libm.* rm *.o Eliminar archivos objeto

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

19

1.9. MAKEFILE Y LA HERRAMIENTA MAKE


Hemos visto un ejemplo sencillo, en el que teclear el comando para compilar y crear el ejecutable es muy sencillo. Sin embargo este procedimiento puede ser largo y tedioso en el caso de grandes proyectos con muchos ficheros fuente y mltiples opciones de compilacin. Por ello existe una herramienta, el make, que haciendo uso de la configuracin de un fichero denominado Makefile (sin extensin, tpicamente situado en la carpeta en la que tenemos el proyecto), se encarga de todo este trabajo. Entre otras cosas, se encarga de realizar la comprobacin de que ficheros han sido modificados, para solo compilar dichos archivos, ahorrando mucho tiempo al usuario. La sintaxis del Makefile es muy potente y compleja, por lo que aqu se realiza solamente la descripcin de una configuracin bsica para el proyecto de esta practica. Para ello crear y editar con el vi el archivo siguiente:
#Makefile del proyecto CC=gcc CFLAGS= -g LIBS= -lm OBJS=misfunc.o principal.o prueba: $(OBJS) $(CC) $(OBJS) $(LIBS) o prueba principal.o: principal.c misfunc.h $(CC) c principal.c misfunc.o: misfunc.c misfunc.h $(CC) c principal.c clean: rm f *.o prueba

Los comentarios en un Makefile se preceden de #


#Makefile del proyecto

El Makefile permite la definicin de variables, mediante una simple asignacin. En la primera parte del Makefile establecemos algunas variables de conveniencia. Se define la cadena CC que nos definir el compilador que se va a usar
CC=gcc

Se define la cadena CFLAGS que nos definir las opciones de compilacin, en este caso habilita la informacin que posibilita la depuracin del ejecutable
CFLAGS= -g

La cadena LIBS almacena las libreras con las que hay que linkar para generar el ejecutable
LIBS= -lm

La cadena OBJS define los mdulos objeto que componen el ejecutable. Aqu se deben listar todos los archivos objeto necesarios, si nos olvidamos alguno, el enlazador encontrara un error.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


OBJS=misfunc.o principal.o

20

A partir de aqu comienzan las reglas, cada regla tiene la siguiente estructura:
objetivo (target): prerequisitos o dependencias comando

Cada regla mira si los prerrequisitos o dependencias han sido modificados, y caso de que lo hayan sido, construye el objetivo utilizando el comando. La siguiente cadena establece la construccin del ejecutable a partir de los objetos, y linkando con las libreras LIBS y generando el ejecutable prueba
prueba: $(OBJS) $(CC) $(OBJS) $(LIBS) o prueba

Que es totalmente equivalente a:


prueba: misfunc.o principal.o gcc misfunc.o principal.o -lm o prueba

Que significa: Si alguno o ambos de los ficheros objeto han cambiado, se tiene que volver a linkar el ejecutable prueba, a partir de los ficheros objeto y enlazando con la librera matemtica lm. A su vez especificamos la compilacin de cada uno de los mdulos objeto:
principal.o: principal.c misfunc.h $(CC) c principal.c misfunc.o: misfunc.c misfunc.h $(CC) c principal.c

Las dos primeras lneas, analizan si han sido modificados principal.c o misfunc.h, y en su caso, significa que hay que volver a compilar el modulo objeto a partir del cdigo fuente. El Makefile analiza las dependencias recursivas, esto es, si el fichero misfunc.h ha sido modificado, primero compilara con las dos ultimas reglas los ficheros objeto principal.o y misfunc.o. Como estos ficheros han sido modificados, invocara a su vez a la regla superior, linkando y obteniendo el ejecutable prueba. La regla clean (make clean) elimina los objetos y el ejecutable
clean: rm f *.o prueba

Lo que significa que si tecleamos en la lnea de comandos: make clean en vez de construir el ejecutable, se borran los archivos binarios temporales y el ejecutable

1.10.

TIPOS DE ERROR
Errores en tiempo de compilacin. Son errores, principalmente de sintaxis. El compilador los detecta y nos informa de ello, no produciendo Universidad Politcnica de Madrid UPM

Existen dos tipos de errores en un programa, errores en tiempo de ejecucin y errores en tiempo de compilacin. Vamos a ver la diferencia entre ambos:

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

21

un ejecutable. Vamos a provocar un error de este estilo. Realizamos el cambio:


printf("Seno de %d es %f \n",i,seno(float(i));

Quitamos el punto y coma del final:


printf("Seno de %d es %f \n",i,seno(float(i))

Y compilamos de nuevo. Nos saldr un mensaje informndonos del error sintctico y en que lnea se produce. Errores en tiempo de ejecucin. Tambin llamados errores lgicos o runtime error. Es un error que no es capaz de detectar el compilador porque no es un fallo en la sintaxis, pero que produce un error al ejecutar el programa por un fallo lgico. Por ejemplo, la divisin por cero, sintcticamente no es un error en el programa, pero al realizar la divisin, se produce un error en tiempo de ejecucin. En todo caso, si el compilador detecta la divisin por cero (por ejemplo al hacer int a=3/0;) puede emitir un warning.

int a=0; int b=3; int c=b/a;

Compilamos este programa y lo ejecutamos. El programa fallara y nos saldr un mensaje informndonos de ello. Tambin cabe la posibilidad de que un fallo en el cdigo del programa produzca un comportamiento no deseado, pero que este no resulte en un fallo fatal y el programa finalice bruscamente.

1.11.
gdb prueba

DEPURACIN DE LA APLICACIN.

Para depurar un programa se debe ejecutar el depurador seguido del nombre del ejecutable (que debe haber sido creado con la opcin g)

El depurador arranca y muestra un nuevo prompt (gdb) que espera a recibir los comandos adecuados para ejecutar el programa paso a paso o como se le indique. Los comandos que puede recibir este prompt se dividen en distintos grupos, mostrados por el comando (gdb) help Si se desea ver los comandos que pertenecen a cada grupo se debe escribir (p.ej. para ver los comandos que permiten gestionar la ejecucin del programa) (gdb) help [nombre grupo] (ejemplo: running )

Y para ver la ayuda de un comando en particular: (gdb) help [comando] Caben destacar por su utilidad los siguientes comandos pertenecientes a los grupos: Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


Tabla 16. Comandos bsicos de gdb

22 Accin comienza la depuracin del programa ejecuta un paso, entrando en funciones ejecuta un paso, sin entrar en funciones termina la ejecucin del programa continua la ejecucin del programa, hasta el siguiente breakpoint muestra el contenido de la variable exp cada vez que el programa se para quita el comportamiento anterior Muestra el contenido de exp inserta un punto de parada o Breakpoint en la lnea correspondiente Eliminan el breakpoint de la lnea correspondiente Pregunta si se desea eliminar todos los breakpoints Muestra informacin acerca de la opcin elegida, por ejemplo info break muestra los breakpoints. sale del debugger

Grupo running

Comando run step next finish continue

data

display [exp] undisplay [exp] print [exp] break [num_linea] clear [num_linea] delete break

breakpoint

status ninguno

info [opcion] quit

Realizar la depuracin del programa anterior, viendo el valor de las posibles variables, ejecutando paso a paso.

1.12.

CREACIN DE UN SCRIPT

Se puede crear un archivo de texto que sirva para ejecutar una serie de comandos consecutivos en el shell, en lo que se llama un script. Para ver un ejemplo se va a crear un script que muestre el nombre de la carpeta actual y a continuacin muestre el contenido de dicha carpeta, para termina ejecutando el programa prueba. Para ello creamos un archivo: vi miscript
echo La carpeta actual es pwd echo Y contiene lo siguiente ls

Si intentamos ejecutar el script, nos dir que no tiene permisos de ejecucin. Para eso realizamos el cambio: chmod a+x miscript Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

23

Debe de quedar claro que con un script no se tiene cdigo mquina, ni se compila, ni se inicia un proceso. Simplemente se la pasan al shell unos comandos en lotes.

1.13.

DESARROLLO EN UN ENTORNO GRAFICO

Existen distintas herramientas para el desarrollo C/C++ en linux, entre las que se podran destacar el Kdevelop, Anjuta, o Eclipse. Para el desarrollo de nuestra aplicacin hemos optado por Kate, que realmente es ms un editor de texto que un entorno de desarrollo, pero sin embargo tiene las caractersticas necesarias para nuestra aplicacin. Kate dispone de resaltado en colores del cdigo, y de plugins, siendo el ms interesante el plugin de make. Dicho plugin permite mediante la pulsacin de ALT+R la invocacin automtica de Makefile (aunque el fichero Makefile lo debemos proveer nosotros), as como la gestin de los posibles errores de compilacin, con la posibilidad de saltar a la lnea del error simplemente haciendo doble click en el mensaje de error.

Figura 14. El editor Kate

1.14.

EJERCICIO PRCTICO

Se suministra en una carpeta un conjunto de ficheros de cdigo fuente, con algunas clases de C++, necesarias para el desarrollo del juego del Tenis, fundamentalmente las clases Mundo, Esfera, Plano, Raqueta, y la clase auxiliar Vector2D. Todas las clases estn completas, exceptuando la clase Mundo.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


#include "Vector2D.h" class Esfera { public: Esfera(); virtual ~Esfera(); Vector2D centro; Vector2D velocidad; float radio; void Mueve(float t); void Dibuja(); };

24

#include "Esfera.h" #include "Vector2D.h" class Plano { public: bool Rebota(Esfera& e); bool Rebota(Plano& p); void Dibuja(); Plano(); virtual ~Plano(); float float float protected: float }; x1,y1; x2,y2; r,g,b; Distancia(Vector2D punto, Vector2D *direccion);

#include "Plano.h" #include "Vector2D.h" class Raqueta : public Plano { public: void Mueve(float t); Raqueta(); virtual ~Raqueta(); Vector2D velocidad; };

class CMundo { public: void Init(); CMundo(); virtual ~CMundo(); void void void void }; InitGL(); OnKeyboardDown(unsigned char key, int x, int y); OnTimer(int value); OnDraw();

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

25

Se solicita al alumno que complete la clase Mundo para obtener el juego del tenis funcional. Se debe escribir un Makefile para la construccin del ejecutable.

1.15.

EJERCICIO PROPUESTO

El alumno debe de completar el juego con alguna funcionalidad extra, como por ejemplo, que cada una de las raquetas sea capaz de disparar un disparo, que cuando impacta al oponente lo inmoviliza, o disminuye el tamao de su raqueta. Tambin se propone el desarrollo de cualquier otro juego de complejidad similar.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

27

2.

INTRODUCCIN A LOS SISTEMAS DISTRIBUIDOS. COMUNICACIN POR SOCKETS

2.1. OBJETIVOS
En el captulo anterior se ha desarrollado el juego bsico del tenis en el que dos jugadores, compartiendo el mismo teclado y el mismo monitor, cada uno con distintas teclas puede controlar su raqueta arriba y abajo para jugar la partida. El objetivo final es la consecucin del juego totalmente distribuido, es decir, cada jugador podr jugar en su propio ordenador, con su teclado y su monitor, y los dos ordenadores estarn conectados por la red. En este captulo se presenta una introduccin a los sistemas distribuidos, los servicios proporcionados en POSIX para el manejo de Sockets, que son los conectores necesarios (el recurso software) para la comunicacin por la red, y su uso en nuestra aplicacin. No pretende ser una gua exhaustiva de dichos servicios sino una descripcin prctica del uso ms sencillo de los mismos, y como integrarlos en nuestra aplicacin para conseguir nuestros objetivos. De hecho, en el curso del captulo se desarrolla una clase C++ que encapsula los servicios de Sockets, permitiendo al usuario un uso muy sencillo de los mismos que puede valer para numerosas aplicaciones, aunque obviamente no para todo. Como primera aproximacin al objetivo final se va a realizar en este captulo la retransmisin del partido de tenis por la red. Esto es, los dos jugadores van a seguir jugando en la misma mquina con el mismo teclado, pero sin embargo otro usuario desde otra mquina podr conectarse remotamente a travs de la red a la mquina y a Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

28

la aplicacin en la que juegan los jugadores (el servidor), y esta le enviara constantemente los datos necesarios para que la mquina remota (el cliente) pueda simplemente dibujar el estado actual de la partida. De esta forma lo que se permite es que los clientes sean meros espectadores de la partida. Inicialmente se plantea la solucin para un nico espectador, y finalmente se aborda la solucin para mltiples espectadores. No obstante esta ltima requerir para su correcto funcionamiento el uso de programacin concurrente (hilos) que se abordara en sucesivos captulos.

RED
Retransmisin partido Servidor, en el que juegan los dos jugadores con el mismo teclado

N posibles clientes que se conectan al servidor para ver el partido

Figura 21. Objetivo del captulo: Retransmisin de la partida de tenis a ordenadores remotos conectados a travs de la red al servidor En sucesivos captulos se completar el desarrollo del juego distribuido haciendo que los jugadores puedan realmente jugar en dos mquinas distintas, que transmitirn los comandos de los jugadores por la misma red al servidor, para que este los ejecute sin necesidad de tener a dichos jugadores utilizando el mismo teclado fsico de la mquina en la que corre el servidor.

2.2. SISTEMA DISTRIBUIDO


Llamaremos sistema distribuido a una solucin software cuya funcionalidad es repartida entre distintas mquinas, teniendo cada mquina su propio procesador (o propios procesadores), su propia memoria, y corriendo su propio sistema operativo. Adems, no es necesario que las mquinas sean iguales, ni ejecuten el mismo SO ni el mismo software. Las mquinas estarn interconectadas por una red que sirve para el intercambio de mensajes entre dichas mquinas.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

29

2.3. SERVICIOS DE SOCKETS EN POSIX


A continuacin se presenta el cdigo de un programa cliente y de un programa servidor, para describir breve y generalmente los servicios de sockets implicados. Este cdigo es prcticamente el ms bsico posible, sin comprobacin de errores. El funcionamiento ser como sigue: Primero se arranca el programa servidor, que inicializa el socket servidor y se queda a la espera de una conexin. A continuacin se debe lanzar el programa cliente que se conectar al servidor. Una vez que ambos estn conectados, el servidor enviara al cliente unos datos (una frase) que el cliente mostrar por pantalla, y a finalmente terminarn ambos programas. El funcionamiento en lneas generales queda representado en la siguiente figura:

Cliente
socket()
Se crea el socket de conexin y comunicacin (es el mismo)

TCP/IP

Servidor
Se crea el socket de conexin

socket()

Se le asigna una direccin y un puerto y se pone a la escucha El socket de conexin se queda bloqueado a la espera Aceptando una conexin

bind() listen()

accept()

connect()

Se conecta a la direccin del servidor

Cuando el cliente se conecta al socket de conexin que esta Aceptando, este devuelve un socket de conexin que es con el que se realiza la comunicacin

send() recv() shutdown() close()


Cierre Cierre Comunicacin Comunicacin

send() recv()

shutdown() close()

Figura 22. Conexin sockets

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

30

2.3.1 Programa cliente


El cdigo del programa cliente bsico es el siguiente:
//includes necesarios para los sockets #include <iostream> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #define INVALID_SOCKET -1 int main() { //declaracion de variables int socket_conn;//the socket used for the send-receive struct sockaddr_in server_address; char address[]="127.0.0.1"; int port=12000; // Configuracion de la direccion IP de connexion al servidor server_address.sin_family = AF_INET; server_address.sin_addr.s_addr = inet_addr(address); server_address.sin_port = htons(port); //creacion del socket socket_conn=socket(AF_INET, SOCK_STREAM,0); //conexion int len= sizeof(server_address); connect(socket_conn,(struct sockaddr *) &server_address,len); //comunicacion char cad[100]; int length=100; //read a maximum of 100 bytes int r=recv(socket_conn,cad,length,0); std::cout<<"Rec: "<<r<<" contenido: "<<cad<<std::endl; //cierre del socket shutdown(socket_conn, SHUT_RDWR); close(socket_conn); socket_conn=INVALID_SOCKET; return 1; }

A continuacin se describe brevemente el programa: Las primeras lneas son algunos #includes necesarios para el manejo de servicios de sockets. En el caso de querer utilizar los sockets en Windows, el fichero de cabecera y la librera con la que hay que enlazar se podran establecer con las lneas:
#include <winsock2.h> #pragma comment (lib, "ws2_32.lib")

En las primeras lneas del main() se declaran las variables necesarias para el socket.
int socket_conn;//the socket used for the send-receive struct sockaddr_in server_address;

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

31

La primera lnea declara el descriptor del socket (de tipo entero) que se utiliza tanto para la conexin como para la comunicacin. La segunda declaracin declara una estructura de datos que sirve para almacenar la direccin IP y el nmero de puerto del servidor y la familia de protocolos que se utilizaran en la comunicacin. La asignacin de esta estructura a partir de la IP definida como una cadena de texto y el puerto definido como un entero se hace como sigue:
char address[]="127.0.0.1"; int port=12000; server_address.sin_family = AF_INET; server_address.sin_addr.s_addr = inet_addr(address); server_address.sin_port = htons(port);

Ntese que la IP que utilizaremos ser la 127.0.0.1. Esta IP es una IP especial que significa la mquina actual (direccin local). Realmente ejecutaremos nuestras 2 aplicaciones (cliente y servidor) en la misma mquina, utilizando la direccin local de la mquina. No obstante esto se puede cambiar. Para ejecutar el servidor en una mquina que tiene la IP 192.168.1.13 por ejemplo, basta poner dicha direccin en ambos programas, ejecutar el servidor en esa mquina, y el cliente en cualquier otra (que sea capaz de enrutar mensajes hacia esa IP). A continuacin se crea el socket, especificando la familia de protocolos (en este caso protocolo de Internet AF_INET) y el tipo de comunicacin que se quiere emplear (fiable=SOCK_STREAM, no fiable=SOCK_DGRAM). En nuestro caso utilizaremos siempre comunicacin fiable.
//creacion del socket socket_conn=socket(AF_INET, SOCK_STREAM,0);

Esta funcin generalmente no produce errores, aunque en algn caso podra hacerlo. Como regla general conviene comprobar su valor, que ser igual a 1 (INVALID_SOCKET) si la funcin ha fallado. A continuacin se intenta la conexin con el socket especificado en la direccin del servidor.
//conexion int len= sizeof(server_address); connect(socket_conn,(struct sockaddr *) &server_address,len);

Esta funcin connect() fallar si no esta el servidor preparado por algn motivo (lo que sucede muy a menudo). Por lo tanto es ms que conveniente comprobar el valor de retorno de connect() para actuar en consecuencia. Se podra hacer algo como:
if(connect(socket_conn,(struct sockaddr *) &server_address,len)!=0) { std::cout<<"Client could not connect"<<std::endl; return -1; }

Si la conexin se realiza correctamente, el socket ya esta preparado para enviar y recibir informacin. En este caso hemos decidido que va a ser el servidor el que enva datos al cliente. Esto es un convenio entre el cliente y el servidor, que adopta el programador cuando disea e implementa el sistema. Como el cliente va a recibir informacin, utilizamos la funcin de recepcin. En esta funcin, se le suministra un buffer en el que guarda la informacin y el nmero de bytes mximo que se espera recibir. La funcin recv() se bloquea hasta que el servidor enve alguna informacin. Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

32

Dicha informacin puede ser menor que el tamao mximo suministrado. El valor de retorno de la funcin recv() es el numero de bytes recibidos.
//comunicacion char cad[100]; int length=100; //read a maximum of 100 bytes int r=recv(socket_conn,cad,length,0); std::cout<<"Rec: "<<r<<" contenido: "<<cad<<std::endl;

Por ultimo se cierra la comunicacin y se cierra el socket.


shutdown(socket_conn, SHUT_RDWR); close(socket_conn); socket_conn=INVALID_SOCKET;

2.3.2 Servidor
El cdigo del programa servidor es algo ms complejo, ya que debe realizar ms tareas. La principal caracterstica es que se utilizan 2 sockets diferentes, uno para la conexin y otro para la comunicacin. El servidor comienza enlazando el socket de conexin a una direccin IP y un puerto (siendo la IP la de la mquina en la que corre el servidor), escuchando en ese puerto y quedando a la espera Accept de una conexin., en estado de bloqueo. Cuando el cliente se conecta, el Accept se desbloquea y devuelve un nuevo socket, que es por el que realmente se envan y reciben datos.
#include <sys/time.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <iostream> #define INVALID_SOCKET -1 int main() { int socket_conn=INVALID_SOCKET;//used for communication int socket_server=INVALID_SOCKET;//used for connection struct sockaddr_in server_address; struct sockaddr_in client_address; // Configuracion de la direccion del servidor char address[]="127.0.0.1"; int port=12000; server_address.sin_family = AF_INET; server_address.sin_addr.s_addr = inet_addr(address); server_address.sin_port = htons(port); //creacion del socket servidor y escucha socket_server = socket(AF_INET, SOCK_STREAM, 0); int len = sizeof(server_address); int on=1; //configuracion del socket para reusar direcciones setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


//escucha bind(socket_server,(struct sockaddr *) &server_address,len); // Damos como maximo 5 puertos de conexion. listen(socket_server,5); //aceptacion de cliente (bloquea hasta la conexion) unsigned int leng = sizeof(client_address); socket_conn = accept(socket_server, (struct sockaddr *)&client_address, &leng); //notese que el envio se hace por el socket de communicacion char cad[]="Hola Mundo"; int length=sizeof(cad); send(socket_conn, cad, length,0); //cierre de los dos sockets, el servidor y el de comunicacion shutdown(socket_conn, SHUT_RDWR); close(socket_conn); socket_conn=INVALID_SOCKET; shutdown(socket_server, SHUT_RDWR); close(socket_server); socket_server=INVALID_SOCKET; return 1; }

33

Hasta la creacin del socket del servidor, el programa es similar al cliente, quitando la excepcin de que se declaran los 2 sockets, el de conexin y el de comunicacin. La primera diferencia son las lneas:
//configuracion del socket para reusar direcciones int on=1; setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));

Estas lneas se utilizan para que el servidor sea capaz de reusar la direccin y el puerto que han quedado abiertos sin ser cerrados correctamente en una ejecucin anterior. Cuando esto sucede, el sistema operativo deja la direccin del socket reservada y por tanto un intento de utilizarla para un servidor acaba en fallo. Con estas lneas podemos configurar y habilitar que se reusen las direcciones previas. La segunda diferencia es que en vez de intentar la conexin con connect(), el servidor debe establecer primero en que direccin va a estar escuchando su socket de conexin, lo que se establece con las lneas:
int len = sizeof(server_address); bind(socket_server,(struct sockaddr *) &server_address,len); // Damos como maximo una cola de 5 conexiones. listen(socket_server,5);

La funcin bind() enlaza el socket de conexin con la IP y el puerto establecidos anteriormente. Esta funcin tambin es susceptible de fallo. El fallo ms comn es cuando se intenta enlazar el socket con una direccin y puerto que ya estn ocupados por otro socket. En este caso la funcin devolver 1, indicando el error. A veces es posible que si no se cierra correctamente un socket (por ejemplo, si el programa finaliza bruscamente), el SO piense que dicho puerto esta ocupado, y al

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

34

volver a ejecutar el programa, el bind() falle, no teniendo sentido continuar la ejecucin. La gestin bsica de este error podra ser:
if(0!=bind(socket_server,(struct sockaddr *) &server_address,len)) { std::cout<<Fallo en el Bind()<<std::endl; return -1; }

La funcin listen() permite definir cuantas peticiones de conexin al servidor sern encoladas por el sistema. Ntese que esto no significa que realmente se atiendan las peticiones de conexin. Es el usuario a travs de la funcin accept() el que acepta una conexin. El numero de conexiones depender de cuantas veces ejecute el programa dicho accept().
//aceptacion de cliente (bloquea hasta la conexion) unsigned int leng = sizeof(client_address); socket_conn = accept(socket_server, (struct sockaddr *)&client_address, &leng);

Lo ms importante del accept() es que en su modo normal bloquea el programa hasta que realmente se realiza la conexin por parte del cliente. A esta funcin se le suministra el socket de conexin, y devuelve el socket que realmente se utilizar para la comunicacin. Si algo falla en la conexin, la funcin devolver 1, lo que corresponde a nuestra definicin de socket invalido INVALID_SOCKET, lo que podemos comprobar:
if(socket_conn==INVALID_SOCKET) { std::cout<<Error en el accept<<std::endl; return -1; }

Una vez que se ha realizado la conexin, la comunicacin se hace por el nuevo socket, utilizando las mismas funciones de envo y recepcin que se podran usar en el cliente. Como en el ejemplo actual, por convenio hemos establecido que ser el servidor el que enva un mensaje al cliente, el cdigo siguiente enva el mensaje Hola Mundo por el socket:
char cad[]="Hola Mundo"; int length=sizeof(cad); //notese que el envio se hace por el socket de communicacion send(socket_conn, cad, length,0);

La funcin send() tambin puede fallar, si el socket no esta correctamente conectado (se ha desconectado el cliente por ejemplo). La funcin devuelve el nmero de bytes enviados correctamente o 1 en caso de error. Tpicamente, si la conexin es buena, la funcin devolver como retorno un valor igual a length, aunque tambin es posible que no consiga enviar todos los datos que se le ha solicitado. Una solucin completa debe contemplar esta posibilidad y reenviar los datos que no han sido enviados. No obstante y por simplicidad, realizamos ahora una gestin sencilla de este posible error:
if(lenght!=send(socket_conn, cad, length,0)) { std::cout<<Fallo en el send()<<std::endl; return -1; }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

35

El cierre de los sockets se realiza de la misma manera que en el cliente, exceptuando que se deben cerrar correctamente los 2 sockets, el de conexin y el de comunicacin. La salida por pantalla al ejecutar las aplicaciones (primero arrancar el servidor y luego el cliente) debera ser (en el lado del cliente): Rec: 11 contenido: Hola Mundo Ntese que los bytes recibidos son 11 porque incluyen el carcter nulo \0 de final de la cadena

2.4. ENCAPSULACIN DE UN SOCKET EN UNA CLASE C++


La API vista en el apartado anterior es C, y aparte de las funciones descritas, tiene otras funcionalidades que no se vern en este curso. Es una prctica habitual cuando se puede desarrollar en C++ encapsular la funcionalidad de la API en una clase o conjunto de clases que oculten parcialmente los detalles ms complejos, facilitando la tarea al usuario. As, por ejemplo, las Microsoft Fundation Classes (MFC) tienen sus clases CSocket y CAsyncSocket para estas tareas. Tambin se pueden encontrar en Internet numerosos envoltorios (wrappers) de C++ para los sockets en linux. Vamos a desarrollar una clase C++ que encapsule la funcionalidad vista en los programas anteriores. Es comn encontrar, bajo una perspectiva estricta de Programacin Orientada a Objetos (POO) que el cliente y servidor se implementan en clases separadas. No obstante, se adopta ahora un enfoque ms sencillo con una sola clase, que utiliza diferentes mtodos en caso del cliente y del servidor. EJERCICIO: Desarrollar la clase Socket, de acuerdo con la cabecera siguiente, para que encapsule los detalles de implementacin anteriores.
//includes necesarios class Socket { public: Socket(); virtual ~Socket(); // 0 en caso de exito y -1 en caso de error int Connect(char ip[],int port); //para el cliente int InitServer(char ip[],int port);//para el servidor //devuelve un socket, el empleado realmente para la comunicacion //el socket devuelto podria ser invalido si el accept falla Socket Accept();//para el servidor void Close();//para ambos //-1 en caso de error, // numero de bytes enviados o recibidos en caso de exito int Send(char cad[],int length); int Receive(char cad[],int length); private: int sock; };

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida El cdigo del servidor se ver simplificado a:
#include <iostream> #include "Socket.h" int main() { Socket servidor; servidor.InitServer("127.0.0.1",12000); Socket conn=servidor.Accept(); char cad[]="Mensaje"; int length=sizeof(cad); conn.Send(cad,length); conn.Close(); servidor.Close(); return 1; }

36

Y el cdigo del cliente:


#include "Socket.h" #include <iostream> int main() { Socket client; client.Connect("127.0.0.1",12000); char cad[1000]; int length=1000; int r=client.Receive(cad,length); std::cout<<"Recibidos: "<<r<<" contenido: "<<cad<<std::endl; client.Close(); return 1; }

2.4.1 Envo de mltiples mensajes


Obviamente, la comunicacin no necesariamente se reduce al envo de un mensaje. Supngase que el servidor lo que quiere enviar es un mensaje 10 veces. Aunque el mensaje podra ser distinto cada vez, para realizar la prueba podemos enviar 10 veces el mismo saludo, quedando el cdigo del servidor como sigue:
char cad[]="Hola Mundo"; int length=sizeof(cad);

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


for(int i=0;i<10;i++) { int err=conn.Send(cad,length); if(err!=length) { std::cout<<"Send error"<<std::endl; break; } }

37

En el lado del cliente podramos conocer que nos van a enviar 10 mensajes y realizar un bucle similar:
char cad[1000]; int length=1000; for(int i=0;i<10;i++) { int r=client.Receive(cad,length); if(r<0) { std::cout<<Error en la recepcion<<std::endl; break; } else std::cout<<"Rec: "<<r<<" contenido: "<<cad<<std::endl; }

No obstante al ejecutar estos programas podramos obtener una salida como la siguiente en el cliente:
Rec: 11 Hola Mundo Rec: 33 Hola Mundo Rec: 66 Hola Mundo Error en la recepcion

Esto se debe a que el servidor enva seguido y todo lo rpido que le permite el bucle for los mensajes, que llegan al cliente. Si el cliente solicita recibir un mensaje de una longitud mxima de 1000 caracteres puede leer efectivamente ms de un mensaje enviado por el servidor. Al sacarlos por pantalla no aparece Hola Mundo Hola Mundo porque hay un terminador de cadena \0 entre ambos. En este punto caben dos alternativa como posibles soluciones a este problema: 1. El servidor enva datos mucho ms despacio de lo que recibe el cliente. En este caso no se suele presentar ningn problema. Supngase que el servidor espera 1 segundo antes de enviar el siguiente mensaje. El cliente ir recibiendo los mensajes por separado sin problemas:
char cad[]="Hola Mundo"; int length=sizeof(cad); for(int i=0;i<10;i++) { usleep(1000000);//espera 1 segundo int err=conn.Send(cad,length); if(err!=length) { std::cout<<"Send error"<<std::endl; break; } }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

38

2. Existe un convenio entre el cliente y el servidor que especifica como son los mensajes, para que el cliente sepa que es lo que va a recibir y como lo tiene que interpretar. Este convenio puede consistir en especificar una longitud fija para los mensajes, o en establecer un carcter terminador de mensaje. En el caso anterior podramos haber recorrido los mensajes buscando los caracteres nulos \0 que nos separaran cada mensaje. Si consideramos los mensajes de longitud fija el cdigo del servidor podra ser:
//definimos los mensajes de 100 bytes siempre char cad[100]="Hola Mundo"; int length=sizeof(cad); //length=100 for(int i=0;i<10;i++) { int err=conn.Send(cad,length); //enviamos 100 bytes if(err!=length) { std::cout<<"Send error"<<std::endl; break; } }

Y el cdigo del cliente quedara:


char cad[100]; int length=100; //vamos a recibir mensajes de 100 bytes for(int i=0;i<10;i++) { int r=client.Receive(cad,length); if(r<0) { std::cout<<Error en la recepcion<<std::endl; break; } else std::cout<<"Rec: "<<r<<" contenido: "<<cad<<std::endl; }

Ntese que aunque se necesitan solo unos pocos bytes para enviar Hola Mundo, realmente se envan muchos ms. Es un enfoque bastante ineficiente, pero muy simple. Se supone que se van a enviar distintos mensajes y que nunca sern ms largos que 100 caracteres. La salida por pantalla es correcta porque se incluye el carcter final de cadena \0, por lo que realmente no se imprimen los 100 caracteres existentes en el buffer.

2.4.2 Conexiones mltiples.


Un servidor puede aceptar ms de una conexin, de tal forma que puede permitir ejecutar varias veces seguidas el mismo cliente, o incluso a distintos clientes desde distintas mquinas. Las conexiones pueden incluso ser simultneas, es decir se puede permitir conectarse a un cliente y cuando termina de comunicar con el, permitir la conexin de otro cliente, o se puede permitir la conexin y comunicacin simultnea con varios clientes. Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida Un cliente solo puede comunicarse con un servidor.

39

2.4.2.1.

Conexiones secuenciales

Una primera opcin es que el servidor atienda secuencialmente las conexiones de los distintos clientes, esto es, se conecta un cliente, se comunica con el y vuelve a esperar aceptando en el accept() a un nuevo cliente.

Servidor
Se crea el socket de conexin

Se le asigna una direccin y un puerto y se pone a la escucha El socket de conexin se queda bloqueado a la espera Aceptando una conexin

Cliente
Se crea el socket de conexin y comunicacin

Conexin

Socket de conexion

Comunicacin

Comunicacin

Cierre Cierre del socket

Seguir aceptando clientes?

SI

NO
Cierre del socket

Figura 23. Servidor que permite mltiples conexiones secuenciales de clientes El cliente permanecera inalterado, y el cdigo del servidor quedara como sigue: Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


#include <iostream> #include "Socket.h" int main() { Socket servidor; servidor.InitServer("127.0.0.1",12000); while(1) { Socket conn=servidor.Accept(); //comunicacion, en este caso envio de 1 unico mensaje char cad[]="Hola mundo"; int length=sizeof(cad); conn.Send(cad,length); conn.Close(); } servidor.Close(); return 1; }

40

La funcin listen() toma sentido en este contexto, ya que permite poner a la cola peticiones de conexiones de varios clientes que intentan la conexin mientras el servidor esta comunicando con el cliente actual. Cuando el servidor vuelve al accept() se atienden dichas peticiones de conexin.

2.4.2.2.

Conexiones simultneas.

Es posible que el servidor acepte la conexin de varios clientes y enve datos a todos ellos, manteniendo la conexin activa con todos simultneamente. Para ello y dado que aun no estamos utilizando programacin concurrente, primero se realiza el accept() de tantos clientes como se vayan a conectar (el servidor debe conocer dicho numero). Hay que recordar que el accept() bloquea hasta que se conecta un cliente, por lo tanto hasta que no se conecten tantos clientes como accept() se intenten, el programa no podr continuar. Como cada conexin devuelve un socket diferente a travs del accept(), estos sockets se pueden almacenar en un vector, y manejar todas las conexiones en el servidor a travs de dicho vector.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida Servidor
Se crea el socket de conexin

41

Se le asigna una direccin y un puerto y se pone a la escucha

ClienteN Cliente1
Se crea el socket de conexin y Se crea el socket i i de conexin y comunicacin Conexion Conexin Comunicacion Comunicacin Cierre Cierre

El socket de conexin se queda bloqueado a la espera Aceptando una conexin

Socket de conexinN Socket de conexin 1

Seguir aceptando clientes?

SI

NO
Comunicacin N Comunicacin 1

Cierre de los N sockets de comunicacin y del de conexin

Figura 24. Comunicacin simultanea con varios clientes El cdigo resultante en el servidor podra ser:
#include <iostream> #include "Socket.h" int main() { Socket servidor; servidor.InitServer("127.0.0.1",12000); Socket conexiones[5]; for(i=0;i<5;i++) conexiones[i]=servidor.Accept(); //comunicacion, en este caso envio de 1 unico mensaje //se envia a los 5 clientes char cad[]="Hola mundo"; int length=sizeof(cad); for(i=0;i<5;i++) conexiones[i].Send(cad,length); for(i=0;i<5;i++) conexiones[i].Close(); servidor.Close(); return 1; }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

42

2.5. ESTRUCTURA DE FICHEROS


Ahora que se ha visto como realizar el envo de informacin por la red, y se dispone de una clase que encapsula la funcionalidad de los sockets se va a proceder a comenzar el desarrollo de la aplicacin distribuida del juego del tenis. Debe quedar claro que solo hay que desarrollar dos aplicaciones, la aplicacin servidor y la aplicacin cliente. La aplicacin servidor se ejecutar una vez, pero la aplicacin cliente (el mismo binario) puede ser ejecutado mltiples veces y en distintas mquinas. Se parte de la aplicacin desarrollada en el tema anterior, que constituye el juego del tenis (los dos jugadores en la misma mquina), cuyos ficheros se encuentran todos en la misma carpeta y los cuales son: Esfera.h y Esfera.cpp (la clase Esfera) Plano.h y Plano.cpp (la clase Plano) Raqueta.h y Raqueta.cpp (la clase Raqueta) Vector2D.h y Vector2D.cpp (la clase Vector2D) Mundo.h y Mundo.cpp (la clase Mundo) Tenis.cpp (el fichero principal con el main() ) Makefile

La primera intencin podra ser duplicar esta carpeta para realizar las modificaciones necesarias en cada una de ellas y transformarlas en el servidor y el cliente. No obstante, esto implicara que habra mucho cdigo idntico duplicado en dos sitios. Por ejemplo, la clase Plano ser exactamente igual en el cliente y en el servidor, su parametrizacin es igual, se dibuja igual. Por tanto no es necesario (de hecho es contraproducente) que el cdigo este repetido. Se pueden desarrollar ambos programas, el cliente y el servidor compartiendo uno o varios archivos de cdigo fuente. Si se analiza la funcionalidad del servidor y del cliente se llega a la conclusin que ambas aplicaciones son iguales, exceptuando: El servidor atiende el teclado, cambiando la velocidad de las raquetas, pero los clientes no, son solo espectadores. Esto se hace en la funcin CMundo::OnKeyboardDown() El servidor cambia las posiciones de los objetos (anima), realiza los clculos de las colisiones. El cliente no tiene que mover los objetos (podra moverlos de forma diferente al servidor), solo tiene que recibir la informacin del servidor de donde estn los objetos en cada instante de tiempo. El cambio de posicin de los objetos se hace en la funcin CMundo::OnTimer().

Como se ve, la nica clase que va a tener diferencias entre el servidor y el cliente es la clase CMundo. Por tanto, se propone nicamente duplicar este archivo con dos nombres diferentes (aunque el nombre de la clase se puede mantener.) Tambin es necesario duplicar el archivo en el que se encuentra el main(), ya que es

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

43

el que instancia la clase CMundo, y en funcin de si es el servidor o el cliente, necesitara hacer un #include a MundoServidor.h o a MundoCliente.h Esfera.h y Esfera.cpp (la clase Esfera) Plano.h y Plano.cpp (la clase Plano) Raqueta.h y Raqueta.cpp (la clase Raqueta) Vector2D.h y Vector2D.cpp (la clase Vector2D) MundoServidor.h y MundoServidor.cpp (la clase Mundo para el servidor) MundoCliente.h y MundoCliente.cpp (la clase Mundo para el cliente) servidor.cpp (el fichero principal con el main(), para el servidor ) cliente.cpp (el fichero principal con el main(), para el cliente ) Makefile

En el Makefile se especifican como se construyen las dos aplicaciones diferentes:


CC=g++ CFLAGS= -g LIBS= -lm -lglut OBJS=Esfera.o Plano.o Raqueta.o Vector2D.o Socket.o HEADERS=Esfera.h MundoCliente.h MundoServidor.h Plano.h Raqueta.h Vector2D.h all: servidor cliente servidor: $(OBJS) MundoServidor.o servidor.o $(CC) $(CFLAGS) -o servidor servidor.o MundoServidor.o $(OBJS) $(LIBS) cliente: $(OBJS) MundoCliente.o cliente.o $(CC) $(CFLAGS) -o cliente cliente.o MundoCliente.o $(OBJS) $(LIBS) Socket.o: Socket.cpp $(HEADERS) $(CC) $(CFLAGS) -c Socket.cpp MundoCliente.o: MundoCliente.cpp $(HEADERS) $(CC) $(CFLAGS) -c MundoCliente.cpp MundoServidor.o: MundoServidor.cpp $(HEADERS) $(CC) $(CFLAGS) -c MundoServidor.cpp Esfera.o: Esfera.cpp $(HEADERS) $(CC) $(CFLAGS) -c Esfera.cpp Plano.o: Plano.cpp $(HEADERS) $(CC) $(CFLAGS) -c Plano.cpp Raqueta.o: Raqueta.cpp $(HEADERS) $(CC) $(CFLAGS) -c Raqueta.cpp Vector2D.o: Vector2D.cpp $(HEADERS) $(CC) $(CFLAGS) -c Vector2D.cpp servidor.o: servidor.cpp $(HEADERS) $(CC) $(CFLAGS) -c servidor.cpp cliente.o: cliente.cpp $(HEADERS) $(CC) $(CFLAGS) -c cliente.cpp clean: rm -f *.o cliente servidor

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida Con este Makefile la simple invocacin make construye tanto el servidor como el cliente

44

2.6. TRANSMITIENDO EL PARTIDO DE TENIS


Inicialmente vamos a realizar el envo de los datos necesarios del servidor a un nico cliente. Para ello se deben de seguir los siguientes pasos:

2.6.1 Conexin
Aadir el Socket de conexin y el de comunicacin en la clase Mundo del servidor:
Socket server; Socket conn;

Aadir el Socket en la clase Mundo del cliente


Socket client;

En la funcin de inicializacin del juego en el servidor se establece la direccin IP y el puerto del servidor y se espera la aceptacin de un cliente:
//en el fichero MundoServidor void CMundo::Init() { //inicializacion de la pantalla, coordenadas, etc server.InitServer("127.0.0.1",12000); conn=server1.Accept(); }

Ntese en este punto que si se compila y ejecuta el servidor no se muestra nada por pantalla. Sencillamente el programa esta bloqueado a la espera de la conexin y ni siquiera ha creado aun la ventana grafica. No obstante el accept se podra realizar ms tarde, despus de haber creado la ventana. El cliente tambin realiza en su funcin init() la conexin del socket:
//del fichero MundoCliente void CMundo::Init() { //inicializacion de la pantalla, coordenadas, etc client.Connect("127.0.0.1",12000); }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

45

2.6.2 Envo de datos


Lo primero es necesario establecer cuales son los datos que es necesario que enve el servidor al cliente. Dado que la pantalla es en su mayora esttica, las variables que es necesario transmitir podran ser: Coordenadas (x, y) de la pelota Coordenadas (x1, y1, x2, y2) de la raqueta del jugador 1. Coordenadas (x1, y1, x2, y2) de la raqueta del jugador 2.

Estos datos deben de ser enviados por el servidor cada vez que se produce un cambio en los mismos, es decir, en cada temporizacin del timer. Cmo se envan datos numricos? Aunque una solucin ms evolucionada se presentara en un tema posterior, una primera solucin sencilla consiste en escribir (sprintf()) estos valores numricos en una cadena de texto y enviar dicha cadena de texto. EJERCICIO: 1. Realizar el envo de los datos por el socket de comunicacin en el servidor (MundoServidor), en la funcin CMundo::OnTimer(), al final de la misma, manteniendo el cdigo existente encargado de realizar la animacin y lgica del juego. 2. Eliminar el cdigo de la funcin CMundo::OnTimer() de MundoCliente y sustituirlo por la recepcin del mensaje del servidor y la extraccin de los valores numricos.

2.7. EJERCICIOS PROPUESTOS


Realizar la retransmisin del juego a un numero fijo de clientes, por ejemplo 3 Implementar los conceptos desarrollados en este tema en un juego de complejidad similar.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

47

3.

COMUNICACIONES Y CONCURRENCIA

3.1. INTRODUCCIN
En el captulo anterior hemos concluido con dos programas, un servidor y un cliente, en el que el servidor enviaba los datos de la partida de tenis de forma continua al cliente. De hecho, tambin podamos permitir que se conectaran varios clientes y despus (una vez conectados todos los clientes, con lo que se tenia que conocer su numero) enviar los datos a todos los clientes. Pero aun no podemos permitir que los clientes espectadores se conecten y desconecten cuando quieran, o que los jugadores puedan efectivamente jugar de forma remota. Tal como esta planteado el programa, esto no es posible hacerlo con programacin convencional (secuencial). Analizaremos en este captulo el porque y veremos la solucin a dichos problemas. Comenzamos analizando un sencillo ejemplo. Supngase que se esta diseando un controlador de una mquina, que se plasma finalmente en un regulador que podra tener el siguiente aspecto (en pseudocdigo):
void main() { float referencia=3.0f; float K=1.2f; while(1) { float medida=GetValorSensor(); float error=referencia-medida; float comando=K*error;//regulador proporcional EnviaComando(comando); } }

Donde las funciones GetValorSensor() y EnviaComando() realizaran la interfaz correspondiente con el hardware de la mquina. Obviamente el programa se tiene que ejecutar de forma continua, recalculando en cada pasada el nuevo error y Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

48

enviando un comando nuevo. El programa anterior utiliza una referencia (el punto al que se quiere llevar el sistema) fija. Supngase que ahora se desea que el usuario sea capaz de introducir por teclado dicha referencia tantas veces como quiera (para llevar la mquina a distintos puntos) y que se programa de la siguiente forma:
void main() { float referencia=3.0f; float K=1.2f; while(1) { printf("Introduzca referencia: "); scanf("%f",&referencia); float medida=GetValorSensor(); float error=referencia-medida; float comando=K*error; EnviaComando(comando); } }

El efecto conseguido es que el programa se queda parado en el scanf() esperando a la entrada del usuario. Cuando el usuario teclea un valor, se calcula y enva un comando a la mquina y el programa se vuelve a quedar parado en el scanf(). Si el usuario no teclea una nueva referencia, la mquina sigue funcionando con el comando anterior de forma indefinida. Obviamente, la solucin anterior no es valida. Tenemos dos tareas diferentes: la ejecucin de forma continua del control y la interfaz con el usuario. Dichas tareas tienen que ejecutarse de forma paralela a la vez. No podemos dejar de ejecutar el control por el hecho de que el usuario este tecleando una referencia, ni podemos inhabilitar al usuario de teclear una referencia por el hecho de que se este ejecutando el control de forma continua. La solucin es utilizar programacin concurrente. En el ejemplo anterior se podra lanzar un hilo dedicado a la gestin de la entrada del usuario mientras que el hilo principal ejecuta el control. El programa en pseudo cdigo podra quedar as:
float referencia=0.0f;//variable global void hilo_usuario() { while(1) { printf("Introduzca referencia: "); scanf("%f",&referencia); } } void main() { float K=1.2f; crear_hilo ( hilo_usuario ); while(1) { float medida=GetValorSensor(); float error=referencia-medida; float comando=K*error; EnviaComando(comando); } }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

49

Ntese como se ha puesto la variable referencia como global, para que ambos hilos tengan acceso a la misma. Los hilos comunican informacin entre ellos a travs de memoria global de la aplicacin.

3.2. REQUISITOS
Vamos a resumir las funcionalidades que nos quedan por implementar en nuestro sistema distribuido: Queremos permitir que los clientes se puedan conectar en el instante que quieran. El servidor no debe quedar bloqueado por esperar a que los clientes se conecten. Queremos permitir cualquier nmero de clientes espectadores. De dichos espectadores, nicamente los dos primeros podrn efectivamente controlar las raquetas. Los dos primeros clientes que se conecten podrn controlar las raquetas, el primero de ellos con las teclas w y s y el segundo con las teclas l y o. El servidor debe de gestionar adecuadamente las desconexiones de los clientes.

3.3. FUNCIONAMIENTO DE GLUT


El funcionamiento bsico de la librera GLUT se plasma en la funcin glutMainLoop(), que es invocada desde el main():
//los callbacks void OnDraw(void); void OnTimer(int value); void OnKeyboardDown(unsigned char key, int x, int y); int main(int argc,char* argv[]) { //Inicializar el gestor de ventanas GLUT //y crear la ventana glutInit(&argc, argv); glutInitWindowSize(800,600); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH); glutCreateWindow("ClienteTenis"); //Registrar los callbacks glutDisplayFunc(OnDraw); glutTimerFunc(25,OnTimer,0); glutKeyboardFunc(OnKeyboardDown); //pasarle el control a GLUT,que llamara a los callbacks glutMainLoop(); return 0; }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

50

Dicha funcin contiene en su interior un bucle continuo (en caso contrario terminara la funcin main() y terminara el programa). Dicho bucle continuo se podra representar a nivel conceptual como:
void glutMainLoop() { while(1) { if(pulsacion_teclado) OnKeyBoardDown(tecla); //la funcion del usuario if(hay_que_dibujar) OnDraw(); //la funcion del usuario if(tiempo_temporizador) OnTimer();//la funcion del usuario } }

Por lo tanto, si se introduce alguna funcin que bloquee la secuencia continua de ejecucin, la aplicacin se vera bloqueada por completo. Por ejemplo, supngase que se ubica un scanf() en la funcin CMundo::OnTimer() para cambiar el radio de la pelota:
void CMundo::OnTimer(int value) { printf("Introduzca el radio: "); scanf("%f",&esfera.radio); jugador1.Mueve(0.025f); jugador2.Mueve(0.025f); esfera.Mueve(0.025f);

El resultado final es la aplicacin bloqueada.

3.3.1 Lanzando un hilo


Podramos conseguir el anterior objetivo, mediante el uso de un hilo, de la siguiente forma:
void* hilo_usuario(void* d) { CMundo* p=(CMundo*) d; while(1) { printf("Introduzca el radio: "); scanf("%f",&p->esfera.radio); } } void CMundo::Init() { //inicializaciones varias pthread_t thid; pthread_create(&thid,NULL,hilo_usuario,this); }

En este caso, la esfera esta contenida dentro de la clase CMundo, sin embargo, el hilo es una funcin global, no es una funcin de la clase CMundo. Para conseguir el Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

51

acceso del hilo al objeto mundo, lo que se puede hacer es pasarle un puntero al mismo aprovechando el cuarto parmetro de la funcin pthread_create(). El hilo se encargara a su vez de hacer el cast correspondiente para poder acceder a los miembros de la clase CMundo.

3.4. ESTRUCTURA DEL SERVIDOR


Se ha visto en los requisitos que es necesario realizar distintas tareas, de forma simultanea: El hilo principal del servidor se encargara de realizar la animacin de la escena (a travs de la funcin OnTimer), del dibujo y de enviar los datos por los sockets a los clientes. Como el envo no es bloqueante, no es necesario crear un hilo para esta tarea. La aceptacin de nuevos clientes si que es bloqueante. Siempre se tiene que estar ejecutando el accept() si queremos que los clientes puedan conectarse y desconectarse cuando quieran. Por lo tanto es necesario un hilo para esta tarea. Para que los clientes remotos puedan efectivamente jugar de forma distribuida, es necesario que enven informacin al servidor. Cada vez que se pulse una tecla, enviaran dicha tecla al servidor. El servidor debe de estar esperando a dicho mensaje. El problema es que la recepcin de mensajes, en principio tambin es bloqueante, por lo que el programa queda bloqueado hasta que se recibe dicho mensaje. La solucin es implementar un hilo para cada uno de los dos jugadores que este a la espera de dichos mensajes.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

52

Programa servidor
//hilo principal OnTimer() { //tareas //animacion //envio //datos //hilo de //aceptacion de //nuevos clientes while(1) { //accept() }

//hilo de //recepcion de //comandos del //jugador1 while(1) { //recv() }

//hilo de //recepcion de //comandos del //jugador2 while(1) { //recv() }

Figura 31. Estructura del servidor Ntese adems que las frecuencias a las que funcionan los distintos hilos son muy variables. El hilo principal ejecuta cada 25 milisegundos, aproximadamente. Sin embargo el hilo de aceptacin de nuevos clientes ejecuta una iteracin del bucle cada vez que se conecta un nuevo cliente, lo que puede tardar de forma variable desde pocos milisegundos a infinito tiempo. Los hilos de recepcin de los comandos de los jugadores funcionan a una frecuencia variable que coincide con las pulsaciones de teclado de los jugadores.

3.5. MLTIPLES CONEXIONES SIMULTANEAS


Para permitir la conexin simultanea de mltiples clientes, es necesario mantener un socket por cada uno de dichos clientes. Para tal efecto declaramos en la clase CMundo (del fichero MundoServidor.h) un vector de la STL de objetos de la clase Socket. Usamos un vector STL porque nos permite de forma cmoda aadir nuevos objetos, quitar elementos y recorrerlo de forma sencilla. Tambin aadimos un mtodo a CMundo denominado GestionaConexiones(), que se encargara de realizar dicha gestin.
class CMundo { public:

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


Socket servidor; std::vector<Socket> conexiones; void GestionaConexiones(); };

53

A continuacin lanzamos un hilo denominado hilo_conexiones(), y de forma similar a como hacamos anteriormente, pasamos un puntero al objeto actual (this) a dicho hilo. Como es interesante manejarnos dentro de la clase mundo, la nica tarea que tiene que hacer la funcin hilo_conexiones()es invocar al mtodo GestionaConexiones(). Dicho mtodo entrara en un bucle infinito en el que se repite un accept(). Cada vez que se conecte un cliente, se le aade al vector de clientes conectados.
void* hilo_conexiones(void* d) { CMundo* p=(CMundo*) d; p->GestionaConexiones(); } void CMundo::GestionaConexiones() { while(1) { Socket s=servidor.Accept(); conexiones.push_back(s); } } void CMundo::Init() { //inicializacion datos servidor.InitServer("127.0.0.1",12000); pthread_t thid_hilo_conexiones; pthread_create(&thid_hilo_conexiones,NULL,hilo_conexiones,this); }

3.6. MOSTRAR LOS CLIENTES CONECTADOS


Una ampliacin interesante al apartado anterior seria mostrar en la ventana los clientes conectados y sus nombres, aparte de los puntos de los dos jugadores. Para ello aadimos un nuevo vector a la clase CMundo del servidor. Tambin transformamos las variables de los puntos de los jugadores en un vector:
class CMundo { public: Socket servidor; std::vector<Socket> conexiones; std::vector<std::string> nombres; void GestionaConexiones(); int puntos[2]; };

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

54

Cada vez que se conecte un cliente nuevo nos deber enviar su nombre, para aadirlo a nuestro vector. Por lo tanto segn se conecta un cliente, esperamos con un Receive() dicho mensaje con el nombre.
void CMundo::GestionaConexiones() { while(1) { Socket s=servidor.Accept(); char cad[100]; s.Receive(cad,100); nombres.push_back(cad); conexiones.push_back(s); } }

Los nombres de los clientes pueden ser mostrados por pantalla:


void CMundo::OnDraw() { char cad[100]; sprintf(cad,"Servidor"); print(cad,300,10,1,0,1); int i; for(i=0;i<nombres.size();i++) { if(i<2) { sprintf(cad,"%s %d",nombres[i].data(),puntos[i]); Print(cad,50,50+20*i,1,0,1); } else { sprintf(cad,"%s",nombres[i].data()); Print(cad,50,50+20*i,1,1,1); } } }

Por supuesto el cliente nos debe enviar el nombre, lo que se puede preguntar al usuario mediante un scanf() al comenzar el programa, y enviarlo inmediatamente despus del Connect(). As el mtodo Init() de la clase CMundo (del cliente) quedara as:
void CMundo::Init() { //inicializacion del mundo char nombre[100]; printf("Introduzca su nombre: "); scanf("%s",nombre); cliente.Connect("127.0.0.1",12000); cliente.Send(nombre,strlen(nombre)+1); }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

55

3.7. RECEPCIN COMANDOS MOVIMIENTO


Cuando el programa cliente detecte una pulsacin de teclado, enviara dicha pulsacin al servidor, para que el servidor la interprete como juzgue necesario. El envo del cliente se realiza fcilmente en la funcin OnKeyboardDown():
void CMundo::OnKeyboardDown(unsigned char key, int x, int y) { char cad[100]; sprintf(cad,"%c",key); cliente.Send(cad,strlen(cad)+1); }

Ntese como este envo se realiza nicamente si el usuario pulsa una tecla. El hilo implementado en el servidor tendr una forma similar al hilo anterior:
void* hilo_comandos1(void* d) { CMundo* p=(CMundo*) d; p->RecibeComandosJugador1(); } void CMundo::RecibeComandosJugador1() { while(1) { usleep(10); if(conexiones.size()>=1) { char cad[100]; conexiones[0].Receive(cad,100); unsigned char key; sscanf(cad,%c,&key); if(key=='s')jugador1.velocidad.y=-4; if(key=='w')jugador1.velocidad.y=4; } } std::cout<<"Terminando hilo comandos jugador1"<<std::endl; } void CMundo::Init() { //Inicializacion server.InitServer("127.0.0.1",12000); pthread_t thid_hilo_conexiones, thid_hilo_comandos1; pthread_create(&thid_hilo_conexiones,NULL,hilo_conexiones,this); pthread_create(&thid_hilo_comandos1,NULL,hilo_comandos1,this); }

Ntese en este caso la comprobacin conexiones.size()>=1, para asegurarnos de que efectivamente existe al menos 1 cliente conectado. Adems se ha aadido un retardo usleep(10) para evitar que el bucle while(1) ejecute en vacio si no hay clientes conectados, lo que supondra una sobrecarga de la CPU innecesaria. EJERCICIO: Compltese el programa, aadiendo un segundo hilo que gestione los comandos del segundo jugador.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

56

3.8. GESTIN DESCONEXIONES


En cualquier instante los clientes espectadores pueden desconectar. Qu pasa entonces con el vector de sockets mantenido por el servidor? Las desconexiones deben de ser analizadas y gestionadas adecuadamente. La forma ms sencilla de detectar las desconexiones es en el envo realizado dentro de la funcin CMundo::OnTimer() en el lado del servidor. El envo hay que hacerlo a todos los clientes conectados. Podramos utilizar el retorno de Send() para realizar la eliminacin del cliente del vector. No obstante hay que tener en cuenta los efectos del borrado sobre el vector que se est recorriendo.
void CMundo::OnTimer(int value) { for(i=0;i<conexiones.size();i++) //MALA SOLUCION { if(-1==conexiones[i].Send(cad,strlen(cad)+1)) { conexiones.erase(conexiones.begin()+i); nombres.erase(nombres.begin()+i); if(i<2) puntos[0]=puntos[1]=0; } } }

La solucin ms sencilla consiste en ir recorriendo el vector al revs, del final al principio, con lo que las eliminaciones no afectan al bucle for.
void CMundo::OnTimer(int value) { for(i=conexiones.size()-1;i>=0;i--) { if(0>=conexiones[i].Send(cad,200)) { conexiones.erase(conexiones.begin()+i); nombres.erase(nombres.begin()+i); if(i<2) puntos[0]=puntos[1]=0; } } }

Ntese que adems, si se ha desconectado uno de los dos primeros clientes (es decir uno de los dos jugadores), entonces el primer espectador pasara a ocupar su lugar y comenzara una nueva partida, poniendo los marcadores a cero.

3.9. FINALIZACIN DEL PROGRAMA


Hasta este punto, cuando se cierra el programa servidor, los hilos acaban de forma forzada. Es conveniente en cualquier programa realizar un cierre ordenado de todos los hilos en ejecucin. Para ello se deben seguir los siguientes pasos (todos ellos en el servidor): Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

57

Aadir una variable denominada acabar, que inicialmente vale 0 a la clase CMundo. Poner dicha variable a 1 en el destructor de la clase CMundo. Utilizar la variable como condicin de repeticin en los bucles while() de los hilos:

while(!acabar) { }

Poner los identificadores de todos los hilos como variables de la clase CMundo, para que puedan ser utilizados en el pthread_join Ejecutar el pthread_join() tantas veces como sea necesario en el destructor de la clase CMundo, para esperar a que terminen los hilos.

En este punto se analiza el resultado cuando se cierra el programa servidor. Realmente se est esperando a la finalizacin de los hilos? La respuesta es no. Los hilos estn bloqueados en el accept() y en el recv() por lo que aunque modifiquemos la bandera acabar esta no es tenida en cuenta hasta la siguiente iteracin del bucle. Hay que conseguir que se desbloqueen el accept() y el recv() de los hilos, lo que se puede hacer de forma sencilla cerrando el socket del servidor, antes de los pthread_join()

3.10.

EJERCICIO PROPUESTO
Realizar la misma tarea con otro juego de complejidad similar. Analizar los posibles problemas de sincronizacin que pueden aparecer en caso de conexiones y desconexiones de clientes. Aumentar la informacin que se retransmite, para que los clientes tengan tambin la informacin de quien esta conectado y quien esta jugando, as como los puntos actuales de la partida.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

59

4.

COMUNICACIN Y SINCRONIZACIN
INTERPROCESO

4.1. INTRODUCCIN
Existen otros mecanismos para comunicar datos entre distintos procesos diferentes a los sockets, cuando los procesos se ejecutan en una mquina con una memoria principal comn y gestionada por un nico sistema operativo (monocomputador). A diferencia de la comunicacin por sockets, que se suele denominar programacin distribuida, estos mecanismos entran dentro de la denominada comunicacin interproceso (Inter Process Comunication IPC). Entre estos mecanismos destacan: Las tuberas sin nombre (pipes) y con nombre (FIFOS) La memoria compartida

El hecho de tener varios procesos (o hilos) accediendo a unos datos comunes de forma concurrente puede originar problemas de sincronizacin en esos datos. Para prevenir estos problemas hay tambin otros mecanismos como: Los mutex y las variables condicionales Las tuberas (usadas para sincronizar) Los semforos

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

60

4.2. EL PROBLEMA DE LA SINCRONIZACION


Cuando existen varios hilos accediendo de forma concurrente a unos datos, se pueden presentar problemas de concurrencia. En nuestra aplicacin, tenemos varios hilos accediendo de forma concurrente al vector de conexiones. En concreto el hilo principal, a travs del timer:
void CMundo::OnTimer(int value) { for(i=conexiones.size()-1;i>=0;i--) { char cad[1000]; sprintf(cad,"%f %f %f %f %f %f %f %f %f %f", esfera.centro.x,esfera.centro.y, jugador1.x1,jugador1.y1, jugador1.x2,jugador1.y2, jugador2.x1,jugador2.y1, jugador2.x2,jugador2.y2); if(0>=conexiones[i].Send(cad,strlen(cad)+1)) { conexiones.erase(conexiones.begin()+i); conectados.erase(conectados.begin()+i); puntos[0]=puntos[1]=0; } } }

El hilo de gestin de las conexiones:


void CMundo::GestionaConexiones() { while(!acabar){ Socket s=server.Accept(); char cad[100]; s.Receive(cad,100); conectados.push_back(cad); conexiones.push_back(s); } }

Y los hilos de recepcin de mensajes de los jugadores:


void CMundo::RecibeComandosJugador1() { Socket s; while(!acabar) { usleep(10); if(conexiones.size()>0) { char cad[100]; conexiones[0].Receive(cad,100); //peligroso printf("Llego la tecla %c\n",cad[0]); unsigned char key=cad[0]; if(key=='s')jugador1.velocidad.y=-4; if(key=='w')jugador1.velocidad.y=4; } } }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

61

Mas concretamente: Es posible que mientras el hilo que recibe los mensajes del jugador decide que hay un jugador conectado (conexiones.size()>0), el hilo principal que enva los datos por el socket, se de cuenta que dicho cliente ha sido desconectado y decida borrarlo del vector. Si el vector queda vacio, un acceso a conexiones[0] genera un error fatal segmentation fault, y nuestro servidor abortara de manera inesperada. No obstante, en la prctica es bastante improbable que suceda esto, y seguramente serian necesarias cientos de conexiones y desconexiones para que este efecto fuera visible. Por lo tanto, no abordaremos de momento el problema de la sincronizacin, pero hay que tener en cuenta que en una aplicacin real sera totalmente obligatorio realizar esta sincronizacin, sino nuestro programa podra fallar en un momento inesperado. Sin embargo si hay un motivo por el que el servidor puede cerrar inesperadamente. Es la recepcin de la seal SIGPIPE cuando se intenta enviar algo por un socket que ha sido cerrado. Si no se gestiona esta seal, el comportamiento por defecto termina el programa. La forma ms sencilla de obviar esta seal, es indicar a la funcin send() en sus banderas, que no enve esta seal en caso de error, lo que se hace de la siguiente forma:
int err=send(sock, cad, length,MSG_NOSIGNAL);

4.3. COMUNICACIN INTERPROCESO


En este tema se propone el siguiente esquema como ejemplo del uso de distintos mecanismos de comunicacin interproceso: Memoria compartida Logger FIFO
TCP/IP

Bot

RED

Servidor

Cliente

Figura 41. Ejemplo de comunicacin interproceso con tuberas y memoria compartida

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

62

En el computador que corre el servidor, se desarrollara un programa que sirva para mostrar eventos de una forma ordenada por pantalla, aunque tambin podra decidir guardarlos a disco, a una base de datos, etc. Los eventos sern los puntos marcados, y quien (el nombre del jugador) que ha marcado un tanto, y sern enviados mediante cadenas de texto por una tubera con nombre o FIFO, al programa que llamaremos logger. En el lado del cliente se desarrollara un programa sencillo que pueda controlar los movimientos de la raqueta correspondiente automticamente. A este programa le llamaremos bot. El cliente y la aplicacin bot intercambiaran datos en una zona de memoria compartida. Ambas aplicaciones nuevas sern aplicaciones de tipo consola. El makefile de las cuatro aplicaciones quedara como sigue:
CC=g++ CPPFLAGS=-g LIBS= -lm -lglut -lpthread OBJS=Esfera.o Plano.o Raqueta.o Vector2D.o Socket.o all: servidor cliente bot logger logger: Logger.o $(CC) $(CPPFLAGS) -o logger Logger.o $(LIBS)

bot: TenisBot.o $(CC) $(CPPFLAGS) -o bot TenisBot.o $(LIBS) servidor: $(OBJS) MundoServidor.o TenisServidor.o $(CC) $(CPPFLAGS) -o servidor MundoServidor.o TenisServidor.o $(OBJS) $(LIBS) cliente: $(OBJS) MundoCliente.o TenisCliente.o $(CC) $(CPPFLAGS) -o cliente MundoCliente.o TenisCliente.o $(OBJS) $(LIBS) depend: makedepend *.cpp -Y clean: rm -f *.o servidor cliente bot logger #DEPENDENCIAS

4.4. TUBERAS CON NOMBRE


Las tuberas son un mecanismo tanto de comunicacin como de sincronizacin. Las tuberas sin nombre o pipes se utilizan en procesos que han sido creados mediante fork() y tienen relaciones padrehijo, de tal forma que heredan dicha tubera. Cuando se trata de procesos totalmente separados, la tubera tiene que ser con nombre para que ambos procesos sean capaces de acceder a ella. Las tuberas con nombre se direccionan como un archivo (un archivo especial) en la estructura de directorios. En las tuberas con nombre tiene que existir un proceso Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

63

que se encargue de crear dicho pseudoarchivo, que adems tiene que ser el primer proceso que comience a ejecutar. Dicho proceso podra tener un cdigo como el siguiente, para enviar por el FIFO una frase a otro proceso que se conecte al mismo:
#include #include #include #include #include #include <stdio.h> <string.h> <signal.h> <fcntl.h> <stdlib.h> <unistd.h>

int main(int argc,char* argv[]) { mkfifo("/home/drodri/Desktop/MiFifo1",0777); int pipe=open("/home/drodri/Desktop/MiFifo1",O_WRONLY); char cad[150]=Hola que tal; int ret=write(pipe,cad,strlen(cad)+1); close(pipe); unlink("/home/drodri/Desktop/MiFifo1"); return 0; }

Donde:
mkfifo("/home/drodri/Desktop/MiFifo1",0777);

crea un archivo con un icono especial en forma de tubera en la ruta indicada, y con los permisos correspondientes (0777= permisos de lectura, escritura y ejecucin para todo el mundo).
int pipe=open("/home/drodri/Desktop/MiFifo1",O_WRONLY);

La funcin open() abre dicha tubera con el acceso especificado (O_WRONLY, O_RDONLY, O_RDWR) y devuelve un descriptor de archivo (pipe) que es el utilizado para enviar y recibir datos. Ntese que esta funcin bloquea hasta que se conecta alguien en el otro extremo de la tubera. A continuacin se hace un envo de datos:
int ret=write(pipe,cad,strlen(cad)+1);

Y finalmente se cierra la tubera y se elimina el pseudoarchivo


close(pipe); unlink("/home/drodri/Desktop/MiFifo1");

El otro proceso nicamente debe de abrir la tubera, usarla y cerrarla, pero no crear el archivo ni borrarlo. Lgicamente, este segundo proceso debe de ser arrancado despus del anterior, para que la tubera sea creada primero antes de intentar abrirla.
int main(void) { int pipe=open("/home/drodri/Desktop/MiFifo1",O_RDONLY); char cad[150]; read(pipe,cad,sizeof(cad));

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


printf("Cadena=%s\n",cad); close(pipe); return 1; }

64

Hay que recordar que la tubera es un mecanismo totalmente unidireccional, no permite que el receptor enve datos por la misma tubera. Si se desea implementar comunicacin bidireccional es necesario el uso de 2 tuberas. Ejercicio: Implementar el programa Logger y los cambios necesarios en el Servidor, para que este enve al Logger el nombre y nmero de puntos que lleva un jugador solo en el momento de marcar un tanto. Seguir los siguientes pasos: 1. El programa Logger se ejecuta antes que el servidor, por lo tanto ser el encargado de crear y destruir el archivo. 2. El logger entra en un bucle infinito de recepcin de datos. 3. Aadir el identificador del FIFO como atributo de la clase CMundo en el servidor. 4. Abrir la tubera (antes de lanzar los hilos) 5. Enviar los datos cuando se produzcan puntos. 6. Cerrar la tubera adecuadamente

4.5. MEMORIA COMPARTIDA


La memoria compartida es un mecanismo exclusivamente de comunicacin que permite tener en comn una zona de memoria, accesible desde varios procesos. Dichos procesos, una vez inicializada y accedida, vern la zona de memoria compartida como memoria propia del proceso. Esta forma de trabajar resulta muy interesante especialmente si la cantidad de datos a compartir entre los distintos procesos es muy elevada. Hay distintas interfases a la memoria compartida, como las funciones de BSD y la memoria compartida POSIX. En este captulo se utiliza la memoria compartida POSIX. As un proceso que quisiera tener en comn una zona de memoria de 10 datos de tipo entero, compartida con otros procesos, podra hacer algo de la forma:
#include #include #include #include #include #include <sys/types.h> <stdio.h> <sys/shm.h> <stdlib.h> <fcntl.h> <string.h>

int main(void) { int datos[10]={0};

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


//memoria compartida key_t mi_key=ftok("/bin/ls",12); int shmid=shmget(mi_key,sizeof(datos),0x1ff|IPC_CREAT); char* punt=(char*)shmat(shmid,0,0x1ff); while(1) { int i,num; printf("Numero de dato: "); scanf("%d",&i); printf("Dato: "); scanf("%d",&num); datos[i]=num; memcpy(punt,datos,sizeof(datos)); } shmdt(punt); shmctl(shmid,IPC_RMID,NULL); return 1; }

65

Donde
key_t mi_key=ftok("/bin/ls",12);

obtiene una llave nica que sirve para identificar la zona de memoria compartida. Los parmetros suministrados a esta funcin tienen que ser los mismos en los diferentes procesos que utilicen la zona de memoria, y son un nombre de archivo (uno cualquiera existente en el sistema de archivos) y un numero entero. A continuacin se obtiene el descriptor mediante la funcin shmget(), a la que se le indica el tamao en numero de bytes de la misma, los permisos (0x1ff significa acceso total a todos). En el caso que el proceso realmente quiera crear la zona porque todava no existe, debe especificar la bandera IPC_CREAT.
int shmid=shmget(mi_key,sizeof(datos),0x1ff|IPC_CREAT);

La obtencin de un puntero, cuyo tipo se puede adaptar sencillamente con un cast, se obtiene con la funcin shmat(), a la que se especifican otra vez los permisos particulares de este acceso.
char* punt=(char*)shmat(shmid,0,0x1ff);

El acceso posterior a la zona de memoria se puede hacer con algn tipo de cast, de indireccin por ndices de un vector o directamente copiando datos a esa zona de memoria. Una vez terminada de utilizar, es necesario soltar el puntero asignado y liberar la zona de memoria:
shmdt(punt); shmctl(shmid,IPC_RMID,NULL);

Como en el caso anterior, el proceso que efectivamente crea la zona de memoria debe de ser arrancado antes que los procesos que accedan a ella. Uno de estos procesos, podra tener el aspecto siguiente:
#include #include #include #include #include <sys/types.h> <stdio.h> <sys/shm.h> <stdlib.h> <fcntl.h>

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


#include <string.h> int main(void) { int datos[10]; int i; key_t mi_key=ftok("/bin/ls",12); int shmid=shmget(mi_key,sizeof(datos),0x1ff); char* punt=(char*)shmat(shmid,0,0x1ff); while(1) { memcpy(datos,punt,sizeof(datos)); for(i=0;i<10;i++) printf("%d ",datos[i]); printf("\n"); } shmdt(punt); return 1; }

66

Para utilizar cmodamente la memoria compartida en nuestra aplicacin, vamos a crear (solo la declaracin es necesaria) una clase de conveniencia que agrupe los distintos datos que se necesitaran compartir entre el cliente y el bot:
#include "Esfera.h" #include "Raqueta.h" class DatosMemCompartida { public: Esfera e; Raqueta r1; Raqueta r2; int jugador;//0 es raqueta1, 1 raqueta 2, otra cosa, espectador int accion; //1 arriba, 0 nada, -1 abajo };

La primera cosa que se observa es que el bot difcilmente podr realizar ninguna decisin sino sabe que raqueta esta controlando (o si esta controlando alguna). Esta informacin tampoco la tiene el cliente, ya que este se limita a transmitir las teclas pulsadas, y el servidor le har caso o no. Para incluir esta informacin, debe de ser el servidor el que enve a todos los clientes el nmero de cliente que son. As si es el cliente 0, sabr que es el primer jugador con la raqueta1, y si es el cliente 1, sabr que es el segundo jugador con la raqueta2.
for(i=conexiones.size()-1;i>=0;i--) { char cad[1000]; sprintf(cad,"%d %f %f %f %f %f %f %f %f %f %f", i,esfera.centro.x,esfera.centro.y, jugador1.x1,jugador1.y1, jugador1.x2,jugador1.y2, jugador2.x1,jugador2.y1, jugador2.x2,jugador2.y2); }

El cliente, tambin aadir una variable denominada num_cliente a la clase Cmundo, y la extraer convenientemente de la cadena recibida.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

67

Es necesario aadir las variables siguientes a la clase CMundo del cliente, para que acceda adecuadamente a la zona de memoria compartida:
#include "DatosMemCompartida.h" class CMundo { public: DatosMemCompartida* datos; int shmid; };

El cliente ser el encargado de crear la zona de memoria compartida, lo que se puede hacer en la funcin Init():
void CMundo::Init() { key_t mi_key=ftok("/bin/ls",12); int shmid =shmget(mi_key,sizeof(DatosMemCompartida),0x1ff|IPC_CREAT); datos=(DatosMemCompartida*)shmat(shmid,0,0x1ff); }

Cada vez que el cliente obtiene datos nuevos los pone en la zona de memoria compartida, para que el bot tenga acceso a ellos:
void CMundo::OnTimer(int value) { char cad[1000]; client.Receive(cad,1000); sscanf(cad,"%d %f %f %f %f %f %f %f %f %f %f", &num_cliente. &esfera.centro.x,&esfera.centro.y, &jugador1.x1,&jugador1.y1, &jugador1.x2,&jugador1.y2, &jugador2.x1,&jugador2.y1, &jugador2.x2,&jugador2.y2); datos->jugador=num_cliente; datos->e.centro=esfera.centro; datos->r1=jugador1; datos->r2=jugador2; }

El bot a su vez, lee los datos de la memoria compartida y toma una decisin acerca de la accin a realizar:
#include #include #include #include <stdio.h> <sys/shm.h> <string.h> "DatosMemCompartida.h"

int main(int argc,char* argv[]) { int i; key_t mi_key=ftok("/bin/ls",12); int shmid=shmget(mi_key,sizeof(DatosMemCompartida),0x1ff);

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


DatosMemCompartida* dat =(DatosMemCompartida*)shmat(shmid,0,0x1ff); while(1) { usleep(25000); if(dat->jugador==0) { dat->accion=0;//accion por defecto, ninguna //completar } if(dat->jugador==1) { } } shmdt(datos); return 0; }

68

Ejercicio: Completar el bot para que tome una decisin del movimiento a realizar El cliente, consultara la accin decidida por el bot y la llevara a cabo. Esta tarea tambin ser llevada a cabo en el timer:
void CMundo::OnTimer(int value) { if(num_cliente==0) { if(datos->accion==-1) OnKeyboardDown('s',0,0); if(datos->accion==1) OnKeyboardDown('w',0,0); datos->accion=0; } if(num_cliente==1) { if(datos->accion==-1) OnKeyboardDown('l',0,0); if(datos->accion==1) OnKeyboardDown('o',0,0); datos->accion=0; } }

Ejercicio: Asegurar que la zona de memoria compartida se libera cuando se cierra la aplicacin cliente. Utilizar una bandera para indicar al bot que tambin debe de cerrarse.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

69

4.6. EJERCICIOS PROPUESTOS


Se proponen a continuacin algunas posibles mejoras a realizar en el juego: Hacer que el jugador pueda controlar mediante su cliente con el teclado la raqueta. nicamente cuando el jugador deja de controlarla durante 10 segundos, entra automticamente el bot y coge el control de nuevo. Realizar un tercer programa en el computador del servidor que permitiera a un comentarista del partido ir tecleando comentarios que fueran guardados en el fichero (mostrados por pantalla en nuestro caso). Tener en cuenta posibles problemas de sincronizacin o gestin de mensajes. Aadir un hilo al programa bot que sirva de interfaz con el usuario y pida al mismo que teclee algn valor que le permita cambiar el comportamiento del bot, ms hbil, menos hbil, por ejemplo.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

70

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

71

Parte II. Programacin avanzada

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

72

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

73

5.

PROGRAMACIN DE CDIGO EFICIENTE

5.1. INTRODUCCIN
Se establece como prerequisito en este libro que el lector conoce el lenguaje C y que es capaz de programar en dicho lenguaje, sintetizando pequeos algoritmos y soluciones. Tambin se asume conocimiento del lenguaje C++ y de conceptos de programacin orientada a objetos. No obstante es bastante posible que el lector todava no tenga en consideracin cuando programa que el cdigo que esta tecleando puede funcionar ms o menos rpido cuando se ejecute en el computador. Hay que tener en cuenta que el computador, PC o microprocesador va ejecutando secuencialmente las instrucciones (ya compiladas en lenguaje mquina), y lo hace de manera tan rpida que los pequeos programas realizados por un aprendiz se ejecutan sin ningn problema. Sin embargo, en el desarrollo de aplicaciones reales, ya sean de gestin, ingeniera o cientficas o incluso ldicas como videojuegos, hay que tener en cuenta que el volumen (cantidad de lneas de cdigo) de dichas aplicaciones es elevadsimo y el microprocesador debe de ejecutar gran cantidad de cdigo. En muchos de estos casos es importante tener en cuenta la eficiencia o cuanto de rpido ejecuta el cdigo que estamos programando. Veamos un ejemplo sencillo, en el que queremos programar una funcin que calcule la exponencial de un nmero real, ya que necesitamos dicha funcin para nuestros clculos ingenieriles. Una forma comn de calcular la exponencial en sistemas informticos es utilizar su desarrollo de Taylor: ex = 1 + x x 2 x3 xn + + + ... + 1! 2! 3! n!

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

74

Parece razonable enfocar la solucin al problema mediante la siguiente descomposicin en funciones, entre las que aparecen de forma lgica la potencia y el factorial. La funcin exponencial recurre a ellas para calcular el trmino nsimo de la serie de Taylor. La estructura del programa con este esquema seria:
#define PRECISION 100 double exponencial(double num); double factorial(int num); double potencia(double base,int expo); void main(void) { double x,e_x; int i; printf("Numero: "); scanf("%lf",&x); e_x=exponencial(x); printf("la exp.de %lf es %lf\n",x,e_x); }

Y la implementacin de las funciones quedara:


double factorial(int valor) { int i; double fact=1; for (i=valor;i>0;i--) fact*=(double)i; return(fact); } double potencia(double base,int expo) { int i; double pot=1; for (i=1;i<=expo;i++) pot*=base; return(pot); } double exponencial(double num) { int i; double resultado=1; for (i=1;i<=PRECISION;i++) resultado+=(potencia(num,i)/factorial(i)); return(resultado); }

Aunque esta solucin es impecable desde el punto de vista estructural (la subdivisin del problema en partes), tiene un importante fallo: una gran ineficiencia a la hora de calcular la exponencial. Considrese cada trmino de Taylor. Se puede apreciar que para calcular el trmino nesimo hay que realizar los siguientes clculos: x n = x x x (n veces) x n ! = n (n 1) (n 2) (n terminos) 2 1 Es decir, para calcular la potencia, hacen falta n multiplicaciones y para calcular el factorial hacen falta otras n multiplicaciones. Por tanto el termino dcimo Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

75

de la serie, requiere 10 veces (20 frente a 2) multiplicaciones que el termino de grado 2. Obviamente, el problema se agrava a medida que se incrementa el nmero de trminos de la serie. Se dice que el coste computacional de calcular cada termino crece linealmente con el ordinal del termino, lo que se representa habitualmente como O(n). No obstante hay una solucin ha este problema, basndose en la recursividad en el calculo del numerador y el denominador de cada termino. Resulta obvio que: x n = x x n 1 n ! = n (n 1)! Lo que implica que el numerador y denominador de cada trmino se pueden calcular a partir del numerador y denominador del trmino anterior. Esta solucin es obviamente mucho ms eficiente, ya que para calcular cada trmino hacen falta nicamente 2 multiplicaciones, una para el numerador y otra para el denominador, independientemente del ordinal del trmino en cuestin. Se dice que cada termino se pude calcular (a partir del numerador y denominador del termino anterior) en tiempo constante (independientemente del ordinal del termino) lo que se representa comnmente como O(1). La implementacin de esta solucin se realiza en una funcin denominada exponencial2():
double exponencial2(double num) { int i; double resultado=1; double numerador=1.0,denominador=1.0f; for (i=1;i<=PRECISION;i++) { numerador*=num; denominador*=i; resultado+=numerador/denominador; } return(resultado); }

Es importante resaltar en este punto que la solucin numrica al problema es exactamente la misma que en el caso anterior. No es una simplificacin, ni una aproximacin, se calcula el mismo resultado pero de dos formas diferentes. Para poner de relieve las diferencias entre ambas soluciones, las ejecutamos cien mil veces cada una. Tngase en cuenta que los clculos que puede hacer una aplicacin real pueden ser muy numerosos, y quizs el calculo de la exponencial puede ser requerido miles de veces.
int main(int argc, char* argv[]) { double num; printf("Numero: "); scanf("%lf",&num); tiempo(); for(int i=0;i<100000;i++) exponencial(num);

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


tiempo(); printf("la exp.de %lf es %lf\n",num,exponencial(num)); for(i=0;i<100000;i++) exponencial2(num); tiempo(); printf("la exp.de %lf es %lf\n",num,exponencial2(num)); return 0; }

76

Para medir los tiempos de ejecucin hemos utilizado una funcin de conveniencia denominada tiempo() que se encarga de sacar por pantalla el tiempo transcurrido entre dos llamadas a la misma:
#include <stdlib.h> #include <sys/timeb.h> void tiempo() { static struct timeb t1={0}; struct timeb t2; ftime(&t2); float t=((t2.time-t1.time)*1000+ (t2.millitm-t1.millitm))/1000.0f; if(t1.time!=0) printf("Tiempo= %f\n",t); t1=t2; }

El resultado de ejecutar el programa anterior podra ser similar al siguiente, que es el resultado de ejecutarlo en un Intel Core2 Duo a 3Ghz con WindowsXP y compilando en Visual C++ 6.0. Estos resultados pueden variar lgicamente en funcin de la mquina, el sistema operativo y el sistema de desarrollo.
Numero: 1 Tiempo= 3.500000 la exp.de 1.000000 es 2.718282 Tiempo= 0.141000 la exp.de 1.000000 es 2.718282

Como se puede apreciar, el tiempo necesario en el primer caso es de 3,5 segundos, frente a los 141 milisegundos que tarda en el segundo caso. Es decir, la segunda solucin es unas 25 veces ms rpida que la primera. Al final, el resultado puede ser una aplicacin que deja a la espera al usuario varios segundos antes de darle un resultado, con la incomodidad que ello supone, si se utiliza el primer enfoque, mientras que en el segundo caso la aplicacin responder mucho ms rpidamente y por tanto la satisfaccin del usuario ser mayor y las probabilidades de xito del software tambin sern incrementadas. El desarrollo de cdigo eficiente y el anlisis de la ejecucin de cdigo es una disciplina mucho ms all de lo que puede cubrir este libro. Este tema trata nicamente de ilustrar algunas tcnicas y ejemplos que introduzcan al lector en este problema, de tal forma que el programador novel empiece a tener en cuenta criterios

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

77

de eficiencia cuando programa, y tenga un punto de partida practico y sencillo a dichas disciplinas.

5.2. MODOS DE DESARROLLO


Es importante resaltar en este punto que los entornos de desarrollo y compiladores permiten fundamentalmente dos modos de desarrollo: La versin de desarrollo, depuracin o Debug es la versin que permite depurar el cdigo en tiempo de ejecucin (con un debugger tpicamente integrado en el entorno). El ejecutable generado no esta optimizado para ejecutarse rpidamente ni para menor tamao. La versin final o Release es para generar un ejecutable optimizado para mayor velocidad, pero que no permite la depuracin del cdigo en busca de errores.

En Visual Studio se puede seleccionar entre ellas en Menu> Build > Set Active Configuration y seleccionar la que se desee. La configuracin por defecto es la Debug. En linux y gcc se utiliza la bandera g para indicar el modo de depuracin. La optimizacin y medida de tiempos de ejecucin se realizan tpicamente en la versin Release, que es la que ejecuta ms rpidamente.

5.3. TIPOS DE OPTIMIZACIONES


Existen cuatro tipos bsicos de optimizaciones que se pueden tratar en el desarrollo de un software: Memoria: intentar minimizar el uso de la memoria utilizada por nuestro programa. Tamao del ejecutable: que el tamao del ejecutable en disco sea lo ms pequeo posible. Eficiencia de ejecucin (procesamiento): que la aplicacin ejecute lo ms rpido posible o utilizando la menor cantidad posible de CPU Tamao datos: Ancho de banda, espacio en disco; que los datos que utiliza, almacena o comunica a travs de cualquier canal sean lo ms reducidos o compactos posibles.

Hay algunas optimizaciones que es capaz de realizar automticamente un buen compilador, como detectar funciones inline o la tcnica de desenrollar bucles o loop unrolling. Sin embargo, el compilador no puede suplir la labor del programador en disear o usar un buen algoritmo, utilizar una estructura de datos eficiente o seleccionar un formato adecuado. Dadas las caractersticas habituales de los computadores actuales, en los que la memoria y el almacenamiento en disco duro son muy abundantes, la optimizacin ms Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

78

comn e importante es la de ejecucin de los programas, y es en la que ms se incide en este tema.

5.4. VELOCIDAD DE EJECUCIN


La velocidad de ejecucin de un procesador se mide generalmente en Mflops o Mflop/s, o millones de operaciones de coma flotante por segundo. Realmente es una medida de procesamiento matemtico, que contabiliza el nmero de adiciones y multiplicaciones de nmeros de coma flotante de precisin doble (64 bits) que realiza un sistema cada segundo. No obstante, una medida interesante y prctica en el desarrollo de aplicaciones es el tiempo total de ejecucin que tarda un determinado algoritmo o cdigo en ejecutar. Estas medidas son las que se realizan en este tema. Este enfoque contabiliza el tiempo total del cdigo, no solo las operaciones aritmticas de coma flotante. Recerdese que gran parte del cdigo se destina a estructuras de control, acceso a memoria, direccionamiento de matrices y vectores, etc. Existe un banco de pruebas (benchmark) denominado el test de Linpack que consiste en la resolucin de un sistema de ecuaciones lineales denso (100, 1000 ecuaciones) mediante el mtodo del pivote parcial, midiendo la eficiencia real en Mflops. Este test de Linpack es el que se utiliza tambin para clasificar los computadores y establecer la lista de los ms rpidos (el Top 500 de los supercomputadores), mediante una medida de eficiencia real en un calculo numrico concreto. Ntese que muchos fabricantes proporcionan una medida de eficiencia de pico o perfecta en base a su arquitectura, su velocidad de reloj, el tamao de su memoria, etc. Pero esto no se ajusta a la eficiencia real, tal y como muestra la siguiente tabla: Tabla 7. Eficiencia en el test de Linpack (100 ecuaciones) para diversos procesadores

Los motivos por los que la eficiencia real no coincide con la de pico son muy numerosos, ya que en la eficiencia de ejecucin influye el algoritmo, las optimizaciones realizadas por el compilador y el programador, el tamao del problema, el sistema operativo, etc. Tambin hay que insistir en que los resultados son una medida parcial del rendimiento del sistema, ya que la eficiencia de un computador en la ejecucin de Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

79

aplicaciones reales depende tambin de otros factores, como la velocidad de acceso a disco, las capacidades de la tarjeta grafica, las comunicaciones con dispositivos, etc.

5.5. ALGUNAS TCNICAS

5.5.1 Casos frecuentes


Considrese la estructura if-else if que maneja distintos casos o condiciones, las cuales puede suceder con distinta probabilidad:
if(condicion1) { //hacer algo1 } else if(condicion2) { //hacer algo2 } else if(condicion3) { //hacer algo3 }

Esta estructura va evaluando las condiciones hasta que encuentra una condicin cierta. Si la condicin 1 es cierta solo es necesario hacer 1 comprobacin, mientras que si es falsa, hacen falta al menos 2 clculos, el de la condicin 1 y el de la 2. Si las condiciones 1 y 2 son falsas, entonces son necesarias 3 operaciones. Se puede decir que el nmero medio de evaluaciones de condicin necesarias es: N m =Pr(condicion1)*1+Pr(condicion2)*2+Pr(condicion3)*3 Pr(condicion)=probabilidad de que sea la primera cierta As, si la probabilidad de que la primera condicin sea cierta es muy pequea, por ejemplo del 1%, al igual que la segunda condicin, mientras que la de la tercera es del 98%, el nmero medio de comprobaciones seria: N m =0,98*3+0,01*2+0,01*1=2,97 Si por el contrario, la probabilidad de la primera condicin fuera del 98%, mientras que la de las otras dos fuera del 1%, el nmero medio de comprobaciones seria: N m =0,01*3+0,01*2+0,98*1=1,03 Una implementacin de prueba se puede realizar obteniendo nmeros aleatorios en el rango 0100, y expresando las condiciones respecto a esos nmeros aleatorios: Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


void casos1() { float valor=100*rand()/(float)RAND_MAX; if(valor>=99.0f) { } else if(valor>=98) { } else if(valor<98.0f) { } } void casos2() { float valor=100*rand()/(float)RAND_MAX; if(valor<98.0f) { } else if(valor<99.0f) { } else if(valor>=99.0f) { } } int main(int argc, char* argv[]) { tiempo(); for(int i=0;i<10000000;i++) casos1(); tiempo(); for(i=0;i<10000000;i++) casos2(); tiempo(); return 0; }

80

El resultado de ejecutar este programa (en la mquina citada anteriormente) seria el siguiente, donde se aprecia la ganancia en tiempo de cmputo:
Tiempo= 0.125000 Tiempo= 0.047000

Hay que resaltar, que al igual que antes el programa realiza exactamente la misma funcin. Esta tcnica se puede resumir como: poner los casos frecuentes primero

5.5.2 Bucles
5.5.2.1. Desenrollado de bucles
El desenrollado de bucles o loop unrolling es una tcnica que consiste en repetir el cdigo interno de un bucle varias veces para evitar precisamente la iteracin de dicho bucle. Ntese que en la mayora de los casos, los bucles solo sirven para evitar la repeticin de cdigo al programador. Pero dicho bucle incurre en un coste Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

81

computacional al realizar las operaciones necesarias. Los compiladores modernos son generalmente capaces de detectar situaciones en las que el loop unrolling es posible y lo realizan automticamente. Cuando el compilador es capaz de detectar que el numero de iteraciones del bucle es fijo (constante), generalmente produce internamente dicho desenrollado, mejorando la eficiencia de ejecucin del cdigo. Solo en pocos casos en los que la eficiencia puede ser critica y el compilador no puede detectarlo, se recurre al loop unrolling manual, en el que el programador lo realiza directamente en cdigo. En aplicaciones criticas (avinica, por ejemplo), esto puede llegar a ser una practica comn. Imagnese un programa que tiene que inicializar un vector de 1000 elementos, cada uno con un valor igual a su ordinal. Tal como ilustra el programa siguiente, eso se puede realizar de la forma tradicional, o realizando un unrolling en este caso de 10 en 10, aunque este tamao puede variar. En el caso de optimizaciones automticas realizadas por el compilador, este decidir el tamao del unrolling.
main(int argc, char* argv[]) { int size=1000; float vector[1000]; //Metodo 1 tiempo(); for(int i=0;i<100000;i++) for(int j=0;j<size;j++) vector[j]=j; //Metodo 2 tiempo(); for(i=0;i<100000;i++) for(int j=0;j<size;j+=10) { vector[j]=j++; vector[j]=j++; vector[j]=j++; vector[j]=j++; vector[j]=j++; vector[j]=j++; vector[j]=j++; vector[j]=j++; vector[j]=j++; vector[j]=j++; } tiempo(); return 0; }

Los tiempos de ejecucin muestran la ventaja de este procedimiento:


Tiempo= 0.266000 Tiempo= 0.093000

5.5.2.2.

Invariantes en bucles

Al ser los bucles tareas repetitivas, en muchos casos un gran nmero de veces, conviene prestar atencin a elementos repetitivos o invariantes dentro de los bucles. El siguiente fragmento de cdigo tiene como objetivo rellenar el vector de 10000 componentes con unos valores que dependen del ordinal del elemento, as como de dos variables a y b, que en este caso se obtienen aleatoriamente (aunque podran Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

82

venir de otra parte de la aplicacin). El valor de dichas variables influyen no solo en el clculo, sino que la relacin entre ambas establece que calculo se debe realizar, tal y como se aprecia en el cdigo siguiente:
void bucle1() { double a=rand(); double b=rand(); double result[10000]; for(int j=0;j<10000;j++) { if(a<b) result[j]=a*j/b; else result[j]=b*j/a; } }

Sin embargo, un anlisis de este cdigo nos muestra que la comparacin del if(a<b) se est realizando 10000 veces de forma innecesaria, ya que los valores de a y b no se modifican dentro del bucle. Por lo tanto, resulta ms eficiente sacar la comparacin de dentro del bucle, y repetir el cdigo del bucle para cada uno de los dos casos resultantes:
void bucle2() { double a=rand(); double b=rand(); double result[10000]; if(a<b) for(int j=0;j<10000;j++) result[j]=a*j/b; else for(int j=0;j<10000;j++) result[j]=b*j/a; }

En este caso, el compilador no ha sido capaz de detectar esta posibilidad, pero si por ejemplo las variables a y b tuvieran valores constantes, el compilador quizas si seria capaz de optimizar. De hecho se puede ir ms lejos y detectar que no solo la comparacin es invariante dentro del bucle, sino que parte de la operacin aritmtica realizada en la asignacin de valores al vector tambin lo es. Por lo tanto podemos tambin extraer dicho clculo y realizarlo una nica vez antes de comenzar los bucles:
void bucle3() { double a=rand(); double b=rand(); double result[10000]; double c=a/b; if(a<b) for(int j=0;j<10000;j++) result[j]=j*c; else for(int j=0;j<10000;j++) result[j]=j/c; }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida Si ejecutamos las tres soluciones miles de veces:
main(int argc, char* argv[]) { tiempo(); for(int i=0;i<10000;i++) bucle1(); tiempo(); for(i=0;i<10000;i++) bucle2(); tiempo(); for(i=0;i<10000;i++) bucle3(); tiempo(); return 0; }

83

Se obtienen los siguientes tiempos:


Tiempo= 2.516000 Tiempo= 1.562000 Tiempo= 0.891000

Una vez ms, la ganancia computacional es visible. Se han presentado en este caso invariantes aritmticos y estructurales, pero tambin es posible extraer la declaracin de objetos dentro de un bucle fuera del mismo para evitar la repetida reserva de memoria. De cualquier forma, tal y como se recomienda en las conclusiones, generalmente no es necesario hacer un anlisis exhaustivo buscando estas posibilidades dentro de los bucles mientras se programa. En general se programan los bucles, si es necesario se analiza el rendimiento y si se aprecia alguna posible mejora significativa y necesaria dentro del bucle, se implementa.

5.5.3 Gestin de memoria


El uso de memoria, la reserva y liberacin de memoria, ya sea utilizando memoria dinmica o utilizando memoria esttica y dejando al compilador realizar la tarea, lleva asociado un coste computacional. Generalmente este coste es muy pequeo, ya que el manejo de memoria suele estar muy optimizado, pero puede ser relevante en aplicaciones que manejen gran cantidad de datos o lo hagan de forma muy repetitiva. Si por ejemplo se desea copiar la informacin de una matriz tridimensional a otra, o asignar todas sus componentes a cero, se podra implementar de la siguiente forma:
int a[3][3][3]; int b[3][3][3]; for(i=0;i<3;i++) for(j=0;j<3;j++) for(k=0;k<3;k++) b[i][j][k] = a[i][j][k]; for(i=0;i<3;i++) for(j=0;j<3;j++) for(k=0;k<3;k++) a[i][j][k] = 0;

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

84

No obstante, usando la contigidad en la reserva de memoria y los mecanismos de asignacin por defecto entre objetos, se puede implementar exactamente lo mismo de una forma mucho ms compacta y ms eficiente en su ejecucin, ya que se evitan los bucles y se realiza directamente una copia de un bloque de memoria en otro:
typedef struct { int element[3][3][3]; } Three3DType; Three3DType a,b; ... b = a; memset(a,0,sizeof(a));

Otra estrategia, cuando se requiere utilizar continuamente datos de dimensin variable es reaprovechar la memoria ya reservada en caso de que sea posible. Una solucin simple consistira en reservar memoria para cada nuevo conjunto de datos (de dimensin n variable), utilizarlos, y a continuacin liberar la memoria utilizada.
while(continuar) { //obtener n int* p=new int[n]; //hacer lo que sea delete [] p; }

Aunque este enfoque es eficiente desde el punto de vista del uso de memoria (siempre se utiliza la mnima cantidad de memoria necesaria), la memoria suele ser muy abundante. Sin embargo el coste computacional de la reserva y liberacin puede ser relevante. En ese caso seria ms conveniente el siguiente enfoque, en el que solo se libera memoria en caso de que no haya suficiente para almacenar los datos, para reservar a continuacin el tamao necesario. Si se tiene un tamao reservado y se necesita menos tamao, no se libera la memoria, sino que directamente se utiliza (desaprovechando una parte). De esta forma, el tamao reservado se estabiliza en el mximo necesario:
int max=0; int* p; while(continuar) { //obtener n if(n>max) { delete [] p; p=new int[n]; } //hacer lo que sea } delete [] p;

Algo similar puede ocurrir por ejemplo usando la Standard Template Library (STL). Si necesitamos crear una cadena de gran tamao, aadiendo uno a uno nuevos caracteres, podramos hacer: Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


std::string cadena; for(i = 0; i < 1000; i++) encoded.append(1, letra);

85

Sin embargo es mucho ms eficiente hacer que la cadena reserve automticamente espacio para 1000 caracteres. De estar forma la adicin (append) de caracteres funciona mucho ms rpido porque no tiene que redimensionar la memoria interna dinmicamente:
std::string cadena; cadena.reserve(1000); for(i = 0; i < 1000; i++) encoded.append(1, letra);

5.5.4 Tipos de datos


Cuando se decide utilizar un tipo de dato u otro, hay que tener en cuenta que esto puede tener consecuencias en el coste computacional. Los procesadores actuales tienen hardware dedicado para realizar operaciones con datos tanto de tipo real como entero. Es posible por tanto, que el procesamiento de nmeros reales de precisin simple (float) se realice ms rpidamente que el de los datos de precisin doble (double). Curiosamente tambin es posible que ciertas operaciones con nmeros enteros se realicen ms rpidamente con enteros de 4 bytes (int) que con enteros de menor tamao (short o char), en ciertos procesadores de 32 bits, ya que su arquitectura esta diseada para este tamao de datos. Por otra parte, la seleccin de un tipo de datos u otro tambin puede tener una seria repercusin en la memoria utilizada, en caso de estructuras de informacin muy grandes. De igual forma, si esos datos se deciden guardar en el disco duro, un tamao muy grande se traducir en un archivo que ocupe mucho espacio, adems del consiguiente tiempo necesario para escribir en el disco, que en general es una operacin relativamente lenta. Tmese como ejemplo las imgenes, como las tomadas por una cmara o un escner. Si una imagen normal tiene millones de pxeles, por ejemplo 1024x768, y cada pxel necesita tpicamente representar la informacin de color (3 componentes), entonces hacen falta aproximadamente 2.4 millones de datos. Las componentes de color admiten una representacin comn como enteros en el rango 0255. Si optamos por utilizar variables de tamao 1 byte (unsigned char), entonces necesitaremos 2.4Mb de memoria para almacenar dicha imagen en memoria. Si por el contrario utilizramos variables de tamao 4 bytes (int), entonces multiplicaramos obviamente por 4, requiriendo aproximadamente 9.4Mb. El gasto de memoria es pues considerable. Si es el programador el que crea nuevos tipos de datos, es conveniente que tenga en cuenta estos criterios. As por ejemplo, en la creacin de un nuevo tipo de datos para representar matrices, puede ser muy interesante que el tipo de datos contemple la posibilidad de codificar explcitamente distintas posibles representaciones especiales de matrices como matrices diagonales, matrices

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

86

simtricas, matrices triangulares o matrices dispersas, aprovechando estas caractersticas especiales para conseguir una mayor eficiencia. La mayora de libreras matemticas existentes que manejan matrices implementan esta funcionalidad.

5.5.5 Tcnicas en C++


El lenguaje de programacin C++ tiene algunas caractersticas que tienen que ser tenidas en cuenta cuando se programa. Por ejemplo, cuando se pasa un objeto a un mtodo por valor, de tal forma que el mtodo no pueda modificar dicho objeto, se realiza una copia del objeto. Si el objeto tiene un tamao en memoria importante se incurre en un coste computacional. Este coste computacional puede ser evitado con el uso de referencias constantes:
void metodo(ClaseA a); //se realiza una copia de a void metodo(const ClaseA& a);//no se realiza copia de a

El polimorfismo es una potente utilidad que puede ser utilizada para realizar una buena ingeniera del software y un buen diseo utilizando patrones. No obstante, hay que tener en cuenta que el polimorfismo (a travs de la virtualidad de mtodos), tiene tambin un coste computacional asociado, ya que la decisin de a que funcin se llama tiene que realizarse en tiempo de ejecucin. Esto no quiere decir que no haya que utilizar el polimorfismo, simplemente que hay que tenerlo en consideracin como posible factor en aplicaciones de uso de CPU muy intensivo, sobre todo si el polimorfismo se encuentra en el ncleo computacional de la aplicacin. La encapsulacin de datos dentro de clases utiliza tpicamente mtodos de acceso a dichos datos. Una vez ms hay que tener en cuenta que la llamada de mtodos tiene un coste computacional asociado. Si se tienen problemas de eficiencia quizs puede ser necesario dejar los datos de una clase como public para poder acceder a ellos directamente. El uso de funciones inline puede mitigar este efecto, ya que el compilador sustituye las llamadas a la funcin por el cdigo que est dentro, en vez de enlazar con ella, tantas veces como sea necesario. As el tamao del ejecutable es algo mayor, pero se evita la sobrecarga de invocacin de funciones. Aunque no se especifique un mtodo como inline, el compilador tiene capacidad para detectar, decidir y compilar como inline dicho mtodo, si con ello calcula que conseguir ms eficiencia. El uso de funciones inline suele recomendarse con funciones de hasta un mximo de 3 lneas. El uso de constructores y destructores es tambin una interesante capacidad de C++, pero tampoco hay que olvidar que los mismos tienen un coste computacional asociado. Si el nmero de construcciones es elevado conveniente tener en cuenta que la inicializacin en la construccin es ms eficiente que la asignacin. As si tenemos la siguiente clase:
ClaseA{ ClaseA(ClaseB b); protected: ClaseB B; };

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida Una implementacin del constructor podra hacer:
ClaseA::ClaseA(ClaseB b) { B=b; }

87

Sin embargo es ms eficiente:


ClaseA::ClaseA(ClaseB b):B(b) { }

5.6. CASOS PRCTICOS


Se presentan en esta seccin algunos ejemplos concretos que permiten profundizar algo ms en algunos conceptos, a la vez que proporcionan una idea de escenarios ms reales de aplicacin.

5.6.1 Algortmica vs. Matemticas


Ahora se quiere realizar un programa que necesita calcular la suma de los n primeros nmeros naturales:

i
i=1

La forma que viene inmediatamente a la cabeza del programador es la utilizacin de un bucle para realizar dicho sumatorio, resultando en:
int Suma1(int n) { int i, sum = 0; for (i = 1; i <= n; i++) sum += i; return sum; }

No obstante, existe una solucin cerrada o analtica a este sumatorio:

i =
i=1

n(n + 1) 2

La implementacin de esta solucin es inmediata:


int Suma2(int n) { int sum = (n * (n+1)) / 2; return sum; }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

88

Para contabilizar algn tiempo distinto de cero (para la segunda solucin), es necesaria la repeticin del clculo 10 millones de veces:
int main(int argc, char* argv[]) { tiempo(); for(int i=0;i<10000000;i++) Suma1(1000); tiempo(); for(i=0;i<10000000;i++) Suma2(1000); tiempo(); return 0; }

La salida por pantalla en la mquina anteriormente descrita es la siguiente:


Tiempo= 8.437000 Tiempo= 0.032000

En este caso queda de relevancia una abismal diferencia entre una solucin u otra. Adems tambin es importante destacar que esta optimizacin de ninguna manera podr ser nunca incluida por el compilador. Se puede concluir que nada sustituye el razonamiento y el conocimiento de un buen ingeniero software. Resaltamos que no es suficiente con ser un buen programador y conocer el lenguaje. Un buen ingeniero software tiene que tener slidas bases de matemticas, fsica, etc.

5.6.2 Generacin de nmeros primos


En el apartado anterior se ha convertido una solucin algortmica en una solucin cerrada o analtica (matemtica). Esto no siempre es posible, y muchas veces una solucin algortmica es totalmente necesaria. No obstante, la importancia de elegir un algoritmo u otro es bien conocida. En problemas tpicos, como la ordenacin de un vector segn algn criterio, se conoce bien que algoritmos funcionan ms rpidamente que otros y se puede elegir entre un conjunto el ms conveniente para nuestra aplicacin. De hecho muchas libreras implementan las distintas opciones para eleccin del usuario. Sin embargo, en numerosas ocasiones el programador tendr que desarrollar su propio algoritmo. Imagnese que se necesita programar una aplicacin que clasifique los primeros N nmeros enteros en primos y no primos. Por motivos de implementacin se decide utilizar un vector de enteros con significado booleano, en el que el ndice u ordinal del elemento corresponde al numero, y el valor del vector (0: falso, no primo, 1: verdadero, primo) corresponde a la clasificacin realizada. Dicho vector es pasado como parmetro a una funcin que es la encargada de rellenar dicho vector con los valores adecuados. En una primera aproximacin, resulta lgico recorrer los N primeros nmeros enteros y para cada uno de ellos estudiar si es primo o no lo es. La forma de hacerlo es comprobar si es divisible por los nmeros enteros menores que el. A priori se supone que el numero es primo (es_primo[i]=1;). Si al realizar una divisin, se comprueba que no lo es, se marca como no primo y se termina la comprobacin. Un sencillo anlisis matemtico revela que no es necesario probar la divisibilidad por Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

89

todos los nmeros inferiores al considerado, sino nicamente por los nmeros hasta la raz cuadrada del nmero considerado.
void Metodo1(int es_primo[],int n) { for(int i=0;i<n;i++) { es_primo[i]=1; for(int j=2;j<=sqrt(i);j++) { if(i%j==0) { es_primo[i]=0; break; } } } }

Sin embargo en este caso existe un enfoque mucho ms eficiente, consistente en una solucin inversa, en la que en vez de ir analizando cada numero si es o no primo mediante divisiones, vamos a ir eliminando nmeros que sabemos que no son primos. La solucin anterior se puede considerar una solucin hacia atrs, mientras que la propuesta ahora es una solucin hacia delante. Es decir, si cogemos el nmero 2, podemos realizar una especie de tabla de multiplicar y concluir rpidamente que los nmeros 4, 6, 8, etc. no son primos. A continuacin podemos repetir el razonamiento con el numero 3, concluyendo que los nmeros 6, 9, 12, etc. tampoco son primos. Podramos proceder as con todos los nmeros, pero ms eficiente aun es hacerlo solo sobre los primos. Si el numero 4 ha sido ya marcado como no primo, entonces lo omitimos del proceso, ya que sus mltiplos (8, 12, 16, etc.) tambin habrn sido ya marcados como no primos, y por lo tanto seria redundante e innecesario. La implementacin de este mtodo quedara como sigue:
void Metodo2(int es_primo[],int n) { for(int i=0;i<n;i++) es_primo[i]=1; i=2; while(i<n) { for(int j=2;i*j<n;j++) //marcar no primos { es_primo[i*j]=0; } do //buscar siguiente primo { i++; } while(i<n && !es_primo[i]); } }

La utilizacin de estos dos mtodos, incluyendo vectores de dimensin dinmica, y la comprobacin de que ambos resultados son idnticos seria:

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


#include #include #include #include #include <stdio.h> <stdlib.h> <math.h> <sys/timeb.h> <memory.h>

90

int main(int argc, char* argv[]) { printf("Introduce n="); int n=0; scanf("%d",&n); int* es_primo=new int[n]; int* es_primo2=new int[n]; //METODO 1 tiempo(); Metodo1(es_primo,n); tiempo(); //METODO 2 Metodo2(es_primo2,n); tiempo(); if(0==memcmp(es_primo,es_primo2,n*sizeof(int))) printf("Iguales\n"); else printf("Error, diferentes\n"); delete [] es_primo; delete [] es_primo2; return 0; }

El resultado de ejecutar este cdigo en la mquina anteriormente descrita es el siguiente:


Introduce n=1000000 Tiempo= 0.953000 Tiempo= 0.047000

Como anteriormente, se pone de relieve una gran ganancia en tiempo de cmputo, ya que el segundo mtodo es unas 20 veces ms rpido, gracias al nuevo mtodo.

5.6.3 Precomputacin de datos


El sensor LMS200 de SICK es un sensor lser que proporciona 181 medidas de distancia (rango) en un intervalo de 180 grados, es decir una medida cada grado. Este sensor se utiliza en numerosas aplicaciones industriales, seguridad, robtica, etc.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

91

Figura 51. Sensor lser LMS200 de SICK Este sensor proporciona de manera continua dichos datos de rangodistancia, que deben de ser procesados por el computador si se desea obtener en coordenadas cartesianas el perfil del objeto escaneado, o las coordenadas de los posibles obstculos u objetos. Una solucin al problema, implementada mediante una funcin podra ser:
void Cartesianas1(double rango[],double x[],double y[]) { for(int i=0;i<=180;i++) { x[i]=rango[i]*cos(i); y[i]=rango[i]*sin(i); } }

Las operaciones ms costosas (o lentas) en el cdigo anterior son las funciones trigonomtricas de seno y coseno. A primera vista parece que no se puede evitar dicho clculo, lo que es cierto. Pero tambin es cierto que entre diferentes llamadas a la funcin, los ngulos de los que se calcula el seno y el coseno son siempre los mismos, de 0 a 180, con intervalos de 1 grado. Por tanto, se puede evitar tener que recalcular dichos valores en cada llamada a la funcin. Para ello podemos optar por precalcular unos vectores declaramos como variables globales por simplicidad. Tngase en cuenta que una solucin real utilizara algn otro mecanismo mejor desde el punto de vista de la ingeniera del software como variables estticas, variables miembro de un clase, etc. El calculo de los valores lo realizamos en una funcin que solo necesitar ser llamada una nica vez. La funcin de clculo de coordenadas cartesianas utilizar ahora los valores precomputados en lugar de recurrir a las funciones matemticas originales.
double sin_alfa[181],cos_alfa[181]; void PrecomputaDatos() { for(int i=0;i<=180;i++) { cos_alfa[i]=cos(i); sin_alfa[i]=sin(i); } }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


void Cartesianas2(double rango[],double x[],double y[]) { for(int i=0;i<=180;i++) { x[i]=rango[i]*cos_alfa[i]; y[i]=rango[i]*sin_alfa[i]; } }

92

Como en otros casos anteriores, cabe resaltar que no estamos haciendo aqu ninguna aproximacin numrica ni simplificacin del problema. El resultado numrico ser idntico para ambas soluciones. Ejecutamos ambos mtodos miles de veces. Tngase en cuenta que esto no difiere mucho de la realidad, ya que en la prctica el sensor esta proporcionando datos de forma continua al computador.
#include #include #include #include <stdio.h> <stdlib.h> <math.h> <sys/timeb.h>

int main(int argc, char* argv[]) { double x[181],y[181],rango[181]; for(int i=0;i<=180;i++) rango[i]=rand()/(float)RAND_MAX;//simular medidas //Metodo 1 tiempo(); for(int j=0;j<10000;j++) Cartesianas1(rango,x,y); //Metodo 2 tiempo(); PrecomputaDatos(); tiempo(); for(j=0;j<10000;j++) Cartesianas2(rango,x,y); tiempo(); return 0; }

En la ejecucin se aprecia que el clculo de las funciones trigonometricas efectivamente tiene un elevado coste asociado. La precomputacion de los valores es prcticamente despreciable, pero supone un gran ahorro de tiempo (unas 20 veces ms rpido)
Tiempo= 0.328000 Tiempo= 0.000000 (precomputo) Tiempo= 0.015000

En otros casos puede resultar que los senos y los cosenos no sean siempre los de los mismos ngulos. Aun as en esos casos se puede implementar una solucin interpolada, en la que se precomputan unas tablas con valores distribuidos siguiendo unos determinados intervalos. Dichas tablas se utilizan mediante interpolacin para cualquier valor intermedio. La solucin as programada es una aproximacin numrica

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

93

la solucin real, con un cierto error. No obstante, la ganancia en velocidad puede suponer una ventaja frente a la precisin numrica. Algunas aplicaciones de grficos interactivos, videojuegos y tarjetas graficas utilizan tcnicas basadas en este concepto.

5.7. OBTENIENDO PERFILES (PROFILING) DEL CDIGO


Aunque se puede analizar un programa midiendo tiempos de la forma que lo hemos hecho, es poco practico. Para analizar la ejecucin de cdigo, existen herramientas (denominadas Profilers) que permiten ejecutar el cdigo, contabilizando las llamadas a las funciones, el tiempo que emplea cada lnea del programa, etc. mostrando informes como resultado de dicho anlisis. En general, cuando se realiza un programa real, en el que el coste computacional es importante, es necesario utilizar un profiler para analizar donde se utilizan los recursos. Es importante resaltar que generalmente mejorando un 20% del cdigo se puede conseguir un 80% de las optimizaciones posibles. Por tanto no merece la pena disear absolutamente todo el cdigo condicionado a la eficiencia. Simplemente analizando los cuellos de botella y mejorando ciertos aspectos se puede conseguir un buen resultado con una carga de trabajo razonable. El Visual Studio tiene incorporado un profiler que permite analizar algunos tiempos de ejecucin de nuestro programa. Para activarlo es necesario ir a Project Settings>Link>Enable profiling. A continuacin se reconstruye el proyecto (Rebuild all). Para ejecutar el profiler, iremos a Menu>Build>Profile. En el programa anterior, se obtiene el siguiente resultado:
Profile: Function timing, sorted by time Date: Thu Jan 15 17:07:04 2009

Program Statistics -----------------Command line at 2009 Jan 15 17:07: "F:\...........\Precomputo" Total time: 251,998 millisecond Time outside of functions: 6,875 millisecond Call depth: 2 Total functions: 7 Total hits: 20006 Function coverage: 71,4% Overhead Calculated 7 Overhead Average 7 Module Statistics for precomputo.exe -----------------------------------Time in module: 245,123 millisecond Percent of time in module: 100,0% Functions in module: 7 Hits in module: 20006 Module function coverage: 71,4%

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


Func Func+Child Hit Time % Time % Count Function --------------------------------------------------------228,48 93,2 228,48 93,2 10000 Cartesianas1(..) (precomputo.obj) 16,001 6,5 16,001 6,5 10000 Cartesianas2() (precomputo.obj) 0,401 0,2 245,12 100,0 1 _main (precomputo.obj) 0,213 0,1 0,213 0,1 4 tiempo(void) (precomputo.obj) 0,023 0,0 0,023 0,0 1 PrecomputaDatos(void) (precomputo.obj)

94

Otros entornos como Matlab, tienen Profilers ms avanzados, que permiten un anlisis ms en profundidad y emiten informes ms completos, incluyendo grficos. Como ejemplo, podemos analizar el siguiente cdigo Matlab:
function pruebaProfile A=randn(10,10); b=randn(10,1); for i=1:20000 x1=inv(A)*b; x2=A\b; if(x1~=x2) disp "error"; display x1; display x2; end B=A*A; C=A+randn(10,10); D=A+C*C; end

Para activar el Profiler y analizar este cdigo, se realiza en la lnea de comandos: >> profile on; >> pruebaProfile >> profile off >> profile report El resultado final es el siguiente. Aunque Matlab tambin tiene un visor especifico para el Profiler, que tambin dispone de ms funcionalidad.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

95

Figura 52. Profiling de una funcin programada en Matlab La figura anterior muestra el tiempo utilizado en cada lnea, tanto en tiempo total como en porcentaje. Se aprecia que la solucin del sistema Ax=b es lo que ms tiempo utiliza. Esta solucin se puede hacer de dos formas posibles, mediante la inversa (x1=Inv(A)*b), o mediante eliminacin de Gauss (x2=A\b). El profiler demuestra como la segunda opcin es ms eficiente, lo que refuerza la importancia de la utilizacin de un algoritmo adecuado para la solucin de un problema.

5.8. CONCLUSIONES
Aunque en este tema se han presentado varias tcnicas de optimizacin para un cdigo ms eficiente, esto no quiere decir que el programador deba perder tiempo en implementar todo su cdigo teniendo en cuenta dicha eficiencia. En este apartado nos gustara pues resumir algunas ideas importantes: No ofuscarse en la eficiencia del cdigo. Segn Donald Knuth premature optimization is the root of all evil. Centrarse en el diseo, la correccin y la ingeniera del software y dejar el problema de la eficiencia para el final, con el uso de un profiler. No hay que asumir que algunas operaciones son ms rpidas que otras. Benchmark everything. Medir tiempos. Utilizar siempre un profiler.

Reducir cdigo no implica siempre eficiencia. Recurdese el loop unrolling Si se pueden tener en cuenta algunas optimizaciones tpicas y sencillas sobre la marcha, como es el paso de parmetros por referencia constante, que es una practica habitual en buenos programadores C++. Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

96

Muchas veces hay que realizar un balance. El uso de optimizaciones para un cdigo ms eficiente a veces es contrapuesto a la buena ingeniera, a la legibilidad y compresin del cdigo, a la encapsulacin, a la modularidad. Otras veces, la velocidad puede requerir mucha memoria, y hay que tomar una decisin de compromiso entre eficiencia de ejecucin y uso de otros recursos. Se recomienda el uso de componentes desarrollados y probados, ya que generalmente estos ya han tenido en cuenta criterios de eficiencia. Estudiar y seleccionar los algoritmos y estructuras de datos ms eficientes para nuestro problema.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

97

6.
6.1. INTRODUCCIN

SERIALIZACIN DE DATOS

La serializacin de datos (marshalling en ingles), es el proceso de codificar un conjunto de informacin o datos (objetos en programacin Orientada a Objetos), en una estructura de informacin lineal o serie de bytes. Este proceso es necesario para almacenar datos en un dispositivo de almacenamiento, enviar datos por mecanismos de comunicacin serie (puertos serie, USB, por red TCP/IP). La serie de bytes puede ser utilizada posteriormente para recuperar la informacin, y volver a generar la estructura de informacin original. La serializacin es pues un mecanismo muy utilizado para transportar objetos por la red, hacer persistente objetos en ficheros o bases de datos, etc. Es por tanto una tcnica necesaria en sistemas distribuidos, pero no se restringe a ellos. Aunque muchos lenguajes de programacin incluyen soporte nativo para serializacin de datos, este soporte puede no ser suficiente en casos de estructuras de informacin dinmicas creadas por el usuario, o en el caso en que el usuario deba decidir que informacin es la relevante para ser transmitida o almacenada y cual no. Siguiendo el planteamiento practico de este libro, se propone un ejemplo como gua de este captulo. Igualmente, este captulo no pretende ser un anlisis riguroso ni una solucin completa al problema de la serializacin de datos, sino simplemente dar al lector una perspectiva del problema y algunas ideas para abordarlo. No obstante, las metodologas presentadas en el captulo pueden ser mas que suficientes para abordar programas relativamente simples como la aplicacin distribuida propuesta en la segunda parte.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

98

En el ejemplo que se propone se tiene una escena (que podra pertenecer a un juego de ordenador, una simulacin, un salva pantallas) consistente en un bosque. Para el dibujo se ha utilizado OpenGL y para la gestin de las ventanas se utiliza la librera GLUT. Dicho bosque esta formado por una serie de rboles en distintas posiciones, con distintas alturas, colores y tamaos de copa. El cdigo correspondiente a la escena de la figura se puede encontrar en el cdigo adjunto a este libro.

Figura 61. Representacin grafica de la escena cuyos datos se van a serializar La declaracin de la clase Bosque es la siguiente. Como se aprecia, todos los atributos de las clases son pblicos. En un buen diseo, esto no debera ser as, pero para nuestro caso se prefiere por simplicidad didctica.
#include "Arbol.h" #define MAX_ARBOLES 100 class Bosque { public: Bosque(); void Aleatorio(int num_arboles); void Dibuja(); void PideDatos(); void Imprime(); int numero; Arbol arbol[MAX_ARBOLES]; };

La clase Arbol, aparte de la posicin, esta fundamentalmente compuesta por un cilindro (tronco) y una esfera (copa):

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


#include "Cilindro.h" #include "Esfera.h" class Arbol { public: void Dibuja(); void PideDatos(); void Imprime(); float x; float y; Cilindro tronco; Esfera copa; };

99

Y por ltimo, las clases Esfera sencillas de las primitivas correspondientes:


class Esfera { public: void Dibuja(); void PideDatos(); void Imprime(); float radio; unsigned short verde,rojo,azul; }; class Cilindro { public: void Dibuja(); void Imprime(); void PideDatos(); Cilindro(); virtual ~Cilindro(); float radio; float altura; };

y Cilindro son parametrizaciones

El diagrama de clases de diseo que representa estas clases es el siguiente:

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

100

Figura 62. Diagrama de Clases de Diseo No obstante, es destacable la estructuracin de los objetos. El siguiente diagrama muestra la disposicin en rbol de la informacin. El bosque esta compuesto por una serie de rboles, y cada uno de ellos tiene su propia copa y su propio tronco. Tambin es de relevancia la distribucin de responsabilidades, y el flujo recursivo de invocaciones. En el diagrama se han mostrado algunos mensajes correspondientes a la responsabilidad de dibujar el entorno. Cuando el gestor de ventanas GLUT decide redibujar, acaba llamando al mtodo Bosque::Dibuja(). Este mtodo a su vez delega, llamando al mtodo Arbol::Dibuja(), para cada uno de los rboles que lo componen. A su vez, cada rbol se dibuja a si mismo, diciendo a sus componentes (el tronco y la copa) que se dibujen. Se puede decir que es una aplicacin del patrn Experto en Informacin, ya que cada objeto es responsable de pintarse a si mismo, dado que el tiene la informacin necesaria para pintarse. As, se va procediendo, avanzando primero en profundidad y luego en anchura en el rbol de informacin representado en la figura.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

101

Figura 63. Estructura de objetos de la aplicacin As, los mtodos correspondientes realizan invocaciones a los mtodos de los objetos que los componen, tal y como se muestra (como ejemplo) para la funcin de dibujo del bosque.
void Bosque::Dibuja() { int i; for(i=0;i<numero;i++) { arbol[i].Dibuja(); } }

Asimismo, tambin existen en las clases funciones que permiten solicitar los datos de un nuevo bosque al usuario para que los teclee por pantalla, mostrar (imprimir) por pantalla los datos de un bosque y generar un bosque aleatorio de un determinado numero de rboles. Supngase en este punto que es necesario almacenar toda la informacin de este bosque en un archivo en el disco duro, para luego poder recuperarlo. O que como la escena forma parte de un juego distribuido, y todos los jugadores se deben mover en la misma escena, es necesario empaquetar en un vector de bytes la escena para enviarla por la red, de tal forma que pueda ser recuperado en un computador remoto. Se plantean a continuacin distintas alternativas.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

102

6.2. REPRESENTACIN OBJETOS EN MEMORIA


Un objeto almacena sus variables de forma contigua en memoria. Supngase que se declara un objeto de la clase Arbol, y se le solicitan los datos al usuario.
Arbol arbol; a.PideDatos();

Al realizar dicha declaracin se reserva un espacio en memoria como el que se ilustra en la figura siguiente, en el que se van reservando recursivamente espacio para las variables y los objetos de los que se compone, con un ordenamiento que sigue el establecido en la declaracin (.h) de la clase.

azul verde rojo radio altura radio y x

copa arbol tronco

Figura 64. Almacenamiento en memoria de un objeto tipo rbol Esta propiedad puede ser utilizada para una fcil serializacin de los datos. Supngase que se quiere almacenar los datos de dicho rbol en un fichero, para su posterior recuperacin. Si se abre el archivo en modo binario y se realiza una escritura sin formato mediante fwrite(), se puede volcar una copia completa de los datos del rbol al archivo.
FILE* f=fopen("Arbol.txt","wb"); fwrite(&arbol,sizeof(Arbol),1,f);

Como el archivo es binario, si se intenta abrir con un editor de texto, no se encontrar ninguna informacin inteligible por el humano. Pero si posteriormente se desea recuperar la informacin de dicho archivo, sobre un objeto de la clase Arbol (que no necesita inicializar ni pedir sus datos, ya que sern asignados en la lectura), basta con realizar los siguientes pasos:
Arbol a; FILE* f=fopen("Arbol.txt","rb"); fread(&a,sizeof(Arbol),1,f);

El mismo razonamiento puede aplicar a todo el bosque, de tal forma que podra ser almacenado en un fichero mediante:
FILE* f=fopen("Bosque.txt","wb"); fwrite(&bosque,sizeof(bosque),1,f);

Y posteriormente recuperado:
FILE* f=fopen("Bosque.txt","rb");

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


fread(&bosque,sizeof(bosque),1,f);

103

De igual forma, si hubiramos deseado enviar los datos del bosque por la red, podramos haber escrito en un vector de bytes la informacin, para posteriormente enviar ese vector de bytes por el socket correspondiente (aunque realmente este es un paso que se puede obviar en este caso)
char* buffer=new char[sizeof(bosque)]; memcpy(buffer,&bosque,sizeof(Bosque));

Y de la misma forma podramos recuperar la informacin del vector mediante:


memcpy(&bosque,buffer,sizeof(Bosque));

Aunque a primera vista podra parecer que ya hemos resuelto el problema de la serializacin, esto no es cierto. En este caso funciona ya que toda la memoria es esttica, incluido el vector de tamao variable del bosque:
int numero; Arbol arbol[MAX_ARBOLES];

No obstante, ya hay un problema de eficiencia importante. Siempre se estn serializando el nmero mximo posible de rboles, aunque nuestro bosque tenga muchos menos. Esto implica un mayor tamao de archivo o un mayor tamao del buffer para enviar por la red, con el consiguiente despilfarro de recursos del sistema. La implementacin ha sido realizada as por simplicidad y evitar la memoria dinmica. Pero en realidad, la capacidad del vector de rboles debera ser gestionada dinmicamente. Que pasara si esto fuera as? Que la copia de memoria no seria valida, ya que se estara copiando nicamente un puntero, que posteriormente no ser valido. Otro motivo por el que este esquema de serializacin puede ser no valido es el hecho de que no se requiera serializar todos los datos de un objeto, sino solo algunos de ellos. Esto es algo muy comn, ya que muchas clases contienen como variables miembro variables auxiliares o temporales que se requieren para el funcionamiento interno de la clase, pero que no tienen mas alcance. Siguiendo el mtodo anterior se serializan el 100% de las variables miembro de la clase, sean relevantes o necesarias o no. En este ultimo caso, tambin se esta incurriendo en un gasto innecesario de los recursos del sistema. Para solucionar estos problemas, el programador puede desarrollar su propia estrategia de serializacin, que le permita gestionar que variables se serializan y cuales no, as como gestionar adecuadamente la memoria dinmica de los objetos. Se presentan a continuacin algunos enfoques tpicos.

6.3. SERIALIZACIN EN C
Aunque la estructura bsica de la aplicacin es Orientada a Objetos, la serializacin tambin tiene que ser realizada en aplicaciones en C. Se presentan en esta seccin algunas tcnicas para realizar esta tarea recurriendo nicamente a funciones de C. Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida Generalmente se puede clasificar la serializacin en:

104

Con formato (texto). La informacin se almacena de tal forma que un humano puede leerla, interpretarla e incluso modificarla fcilmente. Para ello se almacena como cadena de caracteres (ASCII), en la que incluso se pueden almacenar caracteres especiales para facilitar la lectura como tabulaciones y retornos de carro. Sin formato (binaria). La informacin se almacena como un vector de bytes en el que se almacenan byte a byte (en codificacin binaria) todos los datos, sin necesidad de separarlos por caracteres especiales. El resultado es ininteligible por un humano.

Cuando se utiliza el formato, la representacin de un dato del mismo tipo puede tener distinta longitud. Por ejemplo para representar el entero 12 hacen falta solo 2 bytes (2 caracteres, uno para el 1 y otro para el 2), mientras que para el 123456 haran falta 6 bytes. Lo mismo sucede con nmeros de coma flotante como el 0.1 o el 3.1415. Sin embargo en formato binario, los datos ocupan siempre exactamente el mismo tamao. Por ejemplo un entero puede ocupar siempre 4 bytes, al igual que un float. La conclusin es que en general, el formato binario es mas eficiente (necesita menos espacio), ya que adems no necesita separadores, y tiene la ventaja aadida de no tener ninguna perdida de precisin numrica, por redondeos o formatos. Por el contrario presenta la desventaja de no poder ser analizada fcilmente por un humano.

6.3.1 Con formato (texto)


La serializacin con formato en C es bastante tediosa. Por una parte hay que desarrollar cdigo segn se desee serializar a un archivo o a una cadena de texto para su envo por red. Tambin se requiere un uso intensivo de las funciones de manejo de cadenas sprintf(), sscanf(), strcat(), strcpy(), etc., ya que escribir con formato en una cadena no es una tarea obvia. Esta variante no ser desarrollada en este captulo. Se deja al lector como ejercicio, que poda desarrollar fcilmente una vez ledas y comprendidas las secciones siguientes.

6.3.2 Sin formato (binaria)


La serializacin binaria se apoya sobre una serie de macros write que van insertando en un vector de bytes los datos correspondientes, que pueden ser de tipo char (carcter o entero de 1 byte), short (entero de 2 bytes), long (entero de 4 bytes), float (real de 4 bytes) o double (real de 8 bytes). Por cada una de ellas existe la contraria read, que sirve para extraer del vector la variable.
#define #define #define #define #define writeChar(x,y,z){x[y++] = z;} writeShort(x,y,z){*((unsigned short*)((char*)&x[y]))=z; y+=2;} writeLong(x,y,z){*((unsigned long *)((char*)&x[y]))=z; y+=4;} writeFloat(x,y,z){*((float *)((char *)&x[y])) = z; y += 4;} writeDouble(x,y,z){*((double *)((char *)&x[y])) = z; y += 8;}

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


#define #define #define #define #define readChar(x, y, z) {z = x[y++];} readShort(x,y,z){z=*(unsigned short*)((char *)&x[y]); readLong(x,y,z){z=(*(unsigned long*)((char *)&x[y])); readFloat(x, y, z) {z = (*(float *)((char *)&x[y])); readDouble(x, y, z){z = (*(double *)((char *)&x[y])); y y y y += += += +=

105
2;} 4;} 4;} 8;}

Los tres argumentos de la macro son el buffer o vector de bytes, la posicin o ndice del vector de bytes y la variable. Se puede considerar todos ellos como pasados por referencia, ya que la macro puede modificar (y modifica) sus valores. Cabe destacar el aumento automtico del ndice segn el tamao de la variable, de tal forma que el usuario de las macros puede despreocuparse de esta cuenta. Para implementar la funcionalidad de serializacin y deserializacion, seguimos con la estructura establecida para el dibujo y siguiendo el patrn del Experto en Informacin, y aadimos a cada una de las clases (Bosque, Arbol, Cilindro, Esfera) los siguientes mtodos:
void Read(char cad[],int& cont); void Write(char cad[],int& cont);

Ntese que el paso del contador cont a las funciones se hace por referencia, de tal forma que la funcin pueda incrementar dicho contador. La implementacin de estos mtodos para el bosque seria:
void Bosque::Write(char cad[], int& cont) { writeChar(cad,cont,numero); int i; for(i=0;i<numero;i++) arbol[i].Write(cad,cont); } void Bosque::Read(char cad[], int& cont) { readChar(cad,cont,numero); int i; for(i=0;i<numero;i++) arbol[i].Read(cad,cont); }

Ntese como lo primero que hacen las funciones es gestionar el nmero de rboles que componen el bosque. Aunque tambin se pueden plantear otras soluciones que no requieren el almacenamiento explicito de este tamao, su utilizacin simplifica mucho la solucin. Tambin es importante recordar que con estas funciones, ya no importa si el vector de rboles ha sido creado esttica o dinmicamente. La clase Arbol a su vez procede de forma similar:
void Arbol::Write(char cad[], int& cont) { writeFloat(cad,cont,x); writeFloat(cad,cont,y); tronco.Write(cad,cont); copa.Write(cad,cont); }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


void Arbol::Read(char cad[], int& cont) { readFloat(cad,cont,x); readFloat(cad,cont,y); tronco.Read(cad,cont); copa.Read(cad,cont); }

106

En este caso es el programador el que decide el que se serializa, con que formato y en que orden. Es importante por tanto que se respeten estos criterios en el desempaquetamiento de los datos, ya que de no hacerlo el resultado ser incorrecto. No obstante, la implementacin de la deserializacin correspondiente siguiendo el diseo realizado y el patrn Experto en Informacin, se ubica en el mismo lugar, siendo fcil la comprobacin de la necesaria simetra. La serializacin se completa con las funciones correspondientes en Cilindro y Esfera:
void Cilindro::Write(char cad[], int& cont) { writeFloat(cad,cont,radio); writeFloat(cad,cont,altura); } void Cilindro::Read(char cad[], int& cont) { readFloat(cad,cont,radio); readFloat(cad,cont,altura); } void Esfera::Write(char cad[], int &cont) { writeFloat(cad,cont,radio); writeChar(cad,cont,rojo); writeChar(cad,cont,verde); writeChar(cad,cont,azul); } void Esfera::Read(char cad[], int& cont) { readFloat(cad,cont,radio); readChar(cad,cont,rojo); readChar(cad,cont,verde); readChar(cad,cont,azul); }

Una vez realizada esta implementacin, podemos realizar la serializacin de un bosque de la siguiente forma:
bosque.Aleatorio(50); char buffer[3000]; int cont=0; bosque.Write(buffer,cont);

Ntese que en esta implementacin se supone que el buffer tiene capacidad suficiente para almacenar dicha informacin, y no se realiza ninguna comprobacin al respecto. Esto, obviamente, no es una solucin ni valida ni completa, ya que el buffer podra ser pequeo y producirse un desbordamiento, con el consiguiente error en tiempo de ejecucin. En una solucin real se debe al menos comprobar que el tamao del buffer (que puede ser pasado en otro parmetro) es suficiente, aunque tambin seria adecuada la posibilidad de consultar primero el espacio necesario, o utilizar Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

107

memoria dinmica redimensionando el buffer cuando sea necesario. Aun as, la solucin expuesta describe adecuadamente la naturaleza del diseo adoptado, que puede ser fcilmente extensible a dicha comprobacin. La extraccin de la informacin se realizara pues de la siguiente forma:
cont=0; bosque.Read(buffer,cont);

Figura 65. Propagacin de mensajes Write entre los objetos

6.4. SERIALIZACIN EN C++


La serializacin utilizando un lenguaje de mas alto nivel como es C++ es mas sencilla, no solo por el lenguaje en si mismo, sino por las libreras de soporte del mismo. De especial importancia en este caso es la existencia de streams (flujos, aunque los seguiremos llamando streams) en la librera estndar de C++ Standard Template Library (STL). Recurdese la peculiaridad de que para incluir las cabeceras de esta librera no se incluye el .h
#include <iostream> #include <iostream.h> //Include de la STL, OK //Include de librera IO de C++, NO

Entre las clases pertenecientes a la IOStream Library, destacamos las siguientes, que van a ser las utilizadas en nuestro cdigo: istream Stream de entrada Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida ostream Stream de salida ifstream Stream de entrada de fichero. Derivada de istream. ofstream Stream de salida a fichero. Derivada de ostream.

108

istringstream Stream de entrada de cadena (string). Derivada de istream. ostringstream Stream de salida a cadena (string). Derivada de ostream.

Tambin hay que recordar que a esta librera pertenecen los objetos globales cin, cout, cerr y clog (dentro del espacio de nombres std). La potencia de C++ (como el polimorfismo), as como esta librera hacen que programar la serializacin con y sin formato sea bastante ms sencillo.

6.4.1 Con formato (texto)


La serializacin con formato se realiza fcilmente con los operadores de insercin (<<) y extraccin (>>) que ya se encuentra implementado para los tipos bsicos (int, float, etc.), y que se puede sobrecargar fcilmente para tipos de datos (clases) programadas por el usuario. Como dichos operadores no son mtodos de la clase, se declaran como amigos (friend), para que tengan acceso a los posibles atributos protegidos o privados. Aunque en este caso no sea necesario ya que todos los atributos son pblicos, mantenemos la amistad para conseguir una implementacin tpica. Ntese que tanto la insercin como la extraccin admiten un primer parmetro de las clases base istream y ostream, aunque luego se pueden utilizar las clases derivadas segn se desee utilizar un fichero o una cadena.
#include "Arbol.h" #define MAX_ARBOLES 100 #include <iostream> using namespace std; class Bosque { friend istream& operator>>(istream& s, Bosque& b); friend ostream& operator<<(ostream& s, const Bosque& b);

Gracias al using namespace std se evita el tener que anteponer el prefijo std a todas las clases: std::istream, std::ofstream, etc. El segundo parmetro es una referencia en el caso de la extraccin, ya que el operador deber modificar el objeto correspondiente. En el caso de la insercin, el objeto no debe de ser modificada, y por tanto se utiliza una referencia constante.
#include "Cilindro.h" #include "Esfera.h" #include <iostream> using namespace std;

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


class Arbol { friend istream& operator>>(istream& s, Arbol& a); friend ostream& operator<<(ostream& s, const Arbol& a);

109

Ambos operadores devuelven una referencia a istream u ostream, para poder concatenar operaciones:
stream>>a>>b; //stream es un objeto de tipo istream (p. ej. ifstream) stream<<a<<b; //stream es un objeto de tipo ostream (p. ej. ofstream)

La declaracin de los operadores para las clases Esfera y Cilindro es totalmente anloga. La implementacin de los operadores sigue la filosofa anteriormente expuesta, manejando ahora el operador sobrecargado correspondiente:
istream& operator>>(istream& s, Bosque& b) { s>>b.numero; int i; for(i=0;i<b.numero;i++) s>>b.arbol[i]; return s; }

La lectura o extraccin no supone ningn problema, porque en la misma ya se procesan los separadores (recurdese que es una serializacin con formato) como los espacios o retornos de carro. Sin embargo, en la escritura o serializacin es el programador el encargado de establecer dichos separadores. Con el objeto endl se consigue un final de lnea.
ostream& operator<<(ostream& s, const Bosque& b) { s<<b.numero<< endl; int i; for(i=0;i<b.numero;i++) s<<b.arbol[i]<< endl; return s; }

Si queremos escribir dos variables en la misma lnea, entonces tenemos que separarlas por espacios o tabulaciones.
istream& operator>>(istream& s, Arbol& a) { s>>a.x>>a.y; s>>a.tronco; s>>a.copa; return s; } ostream& operator<<(ostream& s, const Arbol& a) { s<<a.x<<" "<<a.y<<std::endl; s<<a.tronco; s<<a.copa; return s; }

La serializacin de la Esfera y el Cilindro quedaran como sigue: Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


istream& operator>>(istream& s, Cilindro& c) { s>>c.radio>>c.altura; return s; } ostream& operator<<(ostream& s, const Cilindro& c) { s<<c.radio<<" "<<c.altura<<std::endl; return s; } istream& operator>>(istream& s, Esfera& e) { s>>e.radio>>e.rojo>>e.verde>>e.azul; return s; } ostream& operator<<(ostream& s, const Esfera& e) { s<<e.radio<<" "<<e.rojo<<" "<<e.verde<<" "<<e.azul<<endl; return s; }

110

Una vez realizada esta implementacin podemos serializar los datos cmodamente desde un fichero, sacarlos por la consola, a una cadenastream, etc.:
cout<<bosque; //a consola ofstream file("Bosque.txt"); file<<bosque; //a un fichero ostringstream str; //a una cadena-stream str<<bosque; string cadena=str.str();//Como obtener la cadena (para enviar //por un socket, por ejemplo)

El resultado de ejecutar la primera lnea de cdigo seria similar a lo siguiente, que por otra parte debera coincidir con el contenido del fichero de texto Bosque.txt. Se aprecian claramente los valores de los atributos respectivos, valores que se podran modificar fcilmente.
50 -9.97497 1.27171 0.2 4.38661 1.80874 117 174 0 -2.99417 7.91925 0.2 5.64568 1.7466 34 233 0 4.21003 0.270699 0.2 4.60799 1.01498 18 156 0

La deserializacin seria igualmente sencilla, sin importar si los datos vienen de un fichero o de una cadenastream (recibida por un socket, por ejemplo).
ifstream file("Bosque.txt"); //desde un fichero file>>bosque; istringstream str; //la cadena coge algun valor str>>bosque; //Desde una stringstream

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

111

6.4.2 Sin formato (binaria)


En el apartado anterior, si deseamos hacer una serializacin binaria por eficiencia, o simplemente porque no queremos que los datos sean fcilmente visibles, podramos intentar abrir el fichero en modo binario:
ofstream file("Bosque.txt",ios::binary);

Pero esto no es suficiente, ya que los operadores insercin y extraccin trabajan sobre los tipos bsicos siempre con formato (en modo texto), y por tanto se serializan de ese modo, aunque el fichero sea abierto en modo binario. Si se desea que la serializacin sea completamente binaria, hay que recurrir a las funciones especificas de la IOStream library que hacen estas tareas. Estas funciones se llaman tpicamente read y write. Aadimos a todas nuestras clases unos mtodos que se llamen de forma similar, y que admitan una referencia a stream. Gracias a esta referencia, podremos utilizar el polimorfismo, y nuestros mtodos funcionarn igual para las clases derivadas correspondientes (fstreams y stringstreams). Los siguientes mtodos sern entonces aadidos a las clases Bosque, Arbol, Esfera y Cilindro:
void Read(std::istream& str); void Write(std::ostream& str);

La filosofa coincide completamente con la desarrollada anteriormente en lenguaje C, a excepcin que ahora se utilizan las funciones de lectura y escritura sin formato (read y write) en un stream:
void Bosque::Read(std::istream& str) { str.read((char*)&numero,sizeof(numero)); int i; for(i=0;i<numero;i++) arbol[i].Read(str); } void Bosque::Write(std::ostream& str) { str.write((char*)&numero,sizeof(numero)); int i; for(i=0;i<numero;i++) arbol[i].Write(str); }

Como anteriormente, preservar el orden es totalmente necesario:


void Arbol::Read(std::istream& str) { str.read((char*)&x,sizeof(float)); str.read((char*)&y,sizeof(float)); tronco.Read(str); copa.Read(str); } void Arbol::Write(std::ostream& str) { str.write((char*)&x,sizeof(float)); str.write((char*)&y,sizeof(float)); tronco.Write(str); copa.Write(str); }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida El resto del cdigo se puede encontrar en las carpetas adjuntas.

112

Si ahora se desea guardar los datos del bosque en un archivo binario, bastara con realizar:
ofstream file("Bosque.txt",ios::binary); bosque.Write(file);

Abriendo el archivo con un editor de textos se puede apreciar que el contenido es totalmente ininteligible. Si posteriormente se desea recuperar los datos del bosque desde dicho archivo se podra hacer:
ifstream file("Bosque.txt",ios::binary); bosque.Read(file);

6.5. CONCLUSIONES
Se ha presentado en este captulo la problemtica de la serializacion de datos y sus aplicaciones en persistencia (ficheros de datos) o comunicaciones. Asimismo se han introducido algunos ejemplos de tcnicas y estrategias que permiten realizar esta tarea de forma ordenada, con el correspondiente cdigo en los lenguajes C y C++. El ejemplo explicado es una aplicacin grafica, pero el uso de la serializacion es mucho mas extenso, tanto que los diseadores de sistemas de desarrollo, libreras y lenguajes ya la tienen en cuenta desde el comienzo, proporcionando dichos servicios de una u otra forma. Aunque en este tema se han explicado tcnicas que permiten al usuario realizar la tarea, se aconseja estudiar en detalle el sistema de desarrollo utilizado y libreras de terceros en el caso de proyectos software reales.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

113

7.
7.1. INTRODUCCIN

BSQUEDAS EN UN ESPACIO DE

ESTADOS MEDIANTE RECURSIVIDAD

La bsqueda y la representacin del conocimiento son dos de los problemas fundamentales de la Inteligencia Artificial (IA). La bsqueda puede formalizarse mediante un espacio de estados que, a su vez, puede verse como un grafo donde los nodos representan estados de dicho espacio y los arcos dirigidos las reglas (operadores, transiciones etc.) que permiten el paso entre estados. La formalizacin de un problema de modo que se pueda resolver mediante algn tipo de bsqueda se denomina representacin del conocimiento. Un espacio de estados para un problema de bsqueda puede formalizarse como una cuadrupla <S, A, I, O> donde S representa el conjunto de estados (o configuraciones) posibles que pueden darse, A las acciones (reglas, operadores etc.) que permiten el paso entre estados, I la configuracin (o estado) inicial y O la configuracin (estado) objetivo a alcanzar. En el caso general, los conjuntos I y O pueden contener ms de un estado.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


1 2 5 3 7 4 1 2 7 8 5 4 6 3 Inicio

114
1 2 8 5 7 6 3 4 Objetivo

a
1 2 5 7 4 3

8 6

2 5 3

7 4

8 6

8 6

2 5

7 4 3

2 5 6

7 4 3

2 3

7 5 4

2 5 3 7 4

8 6

8 6

8 6

Figura 1. Ejemplo de un problema de bsqueda: el puzzle8. En trazo grueso se ha representado el camino solucin. En la Figura 1 se muestra un ejemplo de problema de bsqueda extrado del mundo de los juegos. En el puzzle8, 8 piezas numeradas del 1 al 8 y un hueco comparten una cuadrcula 3x3. El objetivo del juego es obtener una configuracin objetivo a partir de una configuracin de piezas y hueco dada. Las piezas solo pueden moverse en horizontal y vertical ocupando el hueco (casilla sombreada). La figura muestra un posible rbol de bsqueda generado para encontrar la solucin. Los nodos de dicho rbol son las configuraciones intermedias que se atraviesan durante la bsqueda y los arcos (o ramas) los posibles movimientos legales (en el ejemplo dos por cada estado, por lo que el rbol se denomina binario). En la figura se ha regruesado el camino solucin. Un aspecto fundamental de cualquier procedimiento de bsqueda es cmo evadir la explosin combinatoria de estados que pueden aparecer. Por ejemplo, en el puzzle8 una solucin tiene de promedio unos 20 pasos. El factor de ramificacin (el nmero de estados descendientes posibles para un nodo cualquiera del rbol de bsqueda) tiene una media ligeramente menor que 3, con lo que el tamao del espacio de bsqueda est en torno a 320 109 , un nmero muy considerable teniendo en cuenta la aparente simplicidad del problema. En torno a 109 estados seran, pues, los recorridos por un procedimiento de bsqueda sistemtica exhaustivo que, mediante ensayo y error, generara todos los posibles estados intermedios entre el nodo raz y el objetivo. Este es el procedimiento de control de la bsqueda ms sencillo conocido como fuerza bruta. Existen una cantidad importante de algoritmos de control de propsito general para realizar bsquedas exhaustivas, conocidos como tcnicas de bsquedas desinformada (o tambin, bsqueda a ciegas) que conforman un marco genrico para cualquier problema planteado como una bsqueda en un grafo. Esta seccin se centra en la Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

115

implementacin de uno de ellos: la bsqueda primero en profundidad (DepthFirst Search o simplemente DFS) De manera informal, la bsqueda DFS consiste en elegir en todo momento para continuar la bsqueda al candidato que se encuentre a mayor profundidad. En el otro extremo se encuentra la bsqueda primeroenanchura que se decanta por el candidato situado a menor profundidad de entre los posibles. Tomando como ejemplo nuevamente el problema planteado en la Figura 1, y suponiendo que en caso de empates se elije siempre el candidato ms a la izquierda, la seleccin de nodos sera {a, b, d, e, c, f , g} para la bsqueda DFS y {a, b, c, d, e, f, g} para la bsqueda primero en anchura.

7.2. BSQUEDA PRIMERO EN PROFUNDIDAD


Muchos de los algoritmos que recorren grafos se describen con facilidad pero rara es la vez que no presentan dificultades a nivel de detalle. En el caso de bsquedas en grafos los algoritmos de control deben tener especial cuidado con la aparicin de estados repetidos y ciclos. Sin deteccin de ciclos es posible que la bsqueda quede atrapada en un bucle infinito (ver figura 2).

b a c d e

Figura 2: Un grafo de bsqueda que presenta un ciclo. Sin un control de repeticin de estados la bsqueda podra quedar atrapada indefinidamente en {a, d, e, c}. Existen problemas donde la aparicin de los temidos ciclos simplemente no es posible. En estos casos el algoritmo de control se simplifica considerablemente y es ms rpido. A continuacin se describe el algoritmo primeroenprofundidad escrito en pseudocdigo:

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

116

Procedimiento PRIMERO EN PROFUNDIDAD (INICIO, OBJETIVO) Inicializacin: ABIERTOS:={INICIO}, CERRADOS :={} REPETIR hasta alcanzar OBJETIVO o ABIERTOS est vaco 1. Quitar de ABIERTOS el elemento ms a la izquierda y llamarlo X 2. Generar los hijos de X 3. Aadir X a CERRADOS 4. Eliminar aquellos hijos de X que estn en ABIERTOS o en CERRADOS 5. Aadir los hijos de X a ABIERTOS por la izquierda
Como puede apreciarse, el algoritmo es completamente independiente del dominio. La bsqueda parte de un estado inicial INICIO y termina cuando se genera un sucesor que resulta ser el estado OBJETIVO. El bucle de control general lleva implcita dicha comprobacin, que se entiende puede realizarse en tiempo polinomial. De forma intuitiva, el procedimiento elige un candidato de los posibles, genera los sucesores y los guarda como nuevos candidatos a expandir.

7.2.1 Terminologa
El trmino hijo en el pseudocdigo hace referencia a un sucesor directo, empleando la analoga entre un rbol de bsqueda y un rbol genealgico. As, es frecuente utilizar relaciones de parentesco para indicar la profundidad de la relacin (abuelo, bisabuelo, nieto etc.) Un nodo raz del que cuelga un subgrafo ser antecesor de todos los nodos de dicho subgrafo. Anlogamente, dichos nodos sern descendientes de aqul. La relacin de parentesco resulta inadecuada cuando existen ciclos en el grafo (como en el caso de la figura 2). Se denominan hojas a aquellos nodos del rbol de bsqueda que no tienen sucesores. La bsqueda no puede continuar por un nodo hoja teniendo que retroceder en el rbol a algn nodo antecesor, lo que se conoce como vueltaatrs.

7.2.2 Estructuras de datos


A pesar de la aparente sencillez del pseudocdigo se requieren, en el caso general, las siguientes estructuras de datos: Una lista de nodos ABIERTOS: Esta lista puede verse como una cola LIFO (Last In First Out) donde el ltimo elemento que entra es el primer elemento ledo. Si se visualiza la cola como una estructura horizontal donde los datos pueden entrar y salir por ambos extremos izquierda y derecha, una cola LIFO se consigue introduciendo y leyendo datos por el mismo lugar. Unas lista de nodos CERRADOS Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

117

Para cada nodo del rbol hay que almacenar la informacin del camino recorrido. Esto permite recuperar la trayectoria desde el nodo raz una vez alcanzado un estado OBJETIVO. En el algoritmo propuesto basta con almacenar para cada nodo examinado quin es su padre.

La lista ABIERTOS almacena el conjunto de estados generados en cualquier momento de la bsqueda, pero todava no analizados (es decir, se desconocen sus posibles sucesores). Puede perfectamente producirse la paradoja de que un estado OBJETIVO se encuentre en ABIERTOS pero no sea seleccionado para continuar la bsqueda con lo que el procedimiento todava podra tardar un tiempo exponencial en darse cuenta que ya ha encontrado lo que buscaba. El conjunto de nodos por los que la bsqueda puede continuar en cualquier momento se denomina frontera y coincide con la lista de nodos en ABIERTOS para el algoritmo primero en profundidad. La lista CERRADOS corresponde con el conjunto de estados ya examinados (es decir, cuyos sucesores ya han sido generados y se encuentran en ABIERTOS). Esta lista es necesaria para controlar la aparicin de estados repetidos y ciclos durante la bsqueda). Dependiendo del problema particular, es posible que algunas de las estructuras y operaciones indicadas para el algoritmo no sean necesarias. Para ello es necesario realizar un anlisis previo del tipo de rbol de bsqueda que puede generarse. Como ejemplo, en problema de las 3 en raya no pueden producirse estados repetidos (en cada turno aparece una nueva pieza en el tablero). Un algoritmo primeroen profundidad para decidir la mejor jugada en este caso no necesita comprobar si cada estado nuevo ya ha sido generado con anterioridad con lo que la lista CERRADOS es innecesaria El caso del puzzle8 (figura 1) es el caso opuesto. En cada turno es posible realizar un movimiento que genera el nodo padre (en el ejemplo de la figura, el movimiento de la pieza 3 a la izquierda en el estado b, genera el nodo inmediatamente antecesor a. Ntese que en la figura se ha dibujado el grafo de bsqueda sin estados repetidos (es decir, un verdadero rbol). Operadores de transicin entre estados bidireccionales (muy frecuentes en problemas de enrutamiento) generan tambin ciclos durante la bsqueda. Una vez que se detecte esta posibilidad es necesario almacenar todos los estados de la bsqueda recorridos de manera dinmica, si se quiere garantizar que el procedimiento sea completo (es decir, que encuentre una solucin si la hubiere).

7.2.3 Anlisis
El pseudocdigo no presenta grandes dificultades. En cada iteracin de elije un nodo frontera en ABIERTOS (lnea 1), se calculan sus sucesores directos (lnea 2) y se aade dicho nodo a CERRADOS (lnea 3), puesto que ya ha sido analizado. La lnea 4 es necesaria para gestionar sucesores repetidos y ciclos. Un sucesor nuevo repetido puede estar en ABIERTOS (en cuyo caso se ha generado con anterioridad pero an no se han examinado) o en CERRADOS, en cuyo caso se expandi con anterioridad en el grafo. Todos los sucesores recin generados se eliminan si estn bien en ABIERTOS, bien en CERRADOS y solo los que quedan se aaden a la cola (lnea 5). Para conseguir Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

118

que la cola sea LIFO, condicin imprescindible para que la bsqueda sea primero en profundidad, se incluye la direccin de carga y descarga, en este caso por el mismo lado. La figura 3 ejemplifica la generacin de un rbol de bsqueda primero en profundidad para un espacio de estados dado.

a b a b d f f e c

a c d b e a b d e

a c d f c d f b a b e

a c

c e

Figura 3. Ejemplo de bsqueda primeroenprofundidad para el espacio de estados recuadrado en la figura. A medida que la bsqueda avanza el rbol evoluciona de izquierda a derecha y de arriba abajo. En computacin se dice que un algoritmo es correcto si para cualquier solucin candidata que genera, dicha solucin satisface las especificaciones del problema. Ms fuerte es el requisito de completitud. Un algoritmo se dice que es completo cuando si existe una solucin la encuentra. Es interesante destacar que, de forma un tanto sorprendente, la bsqueda primero en profundidad (DFS) no garantiza la completitud en el caso general. Esto es as porque cabe la posibilidad de que el algoritmo se pierda en ramales de profundidad infinita y nunca llegue a examinar el camino o caminos que llevan al estado OBJETIVO. Imagine el lector que quiere saber si es un descendiente directo de Abraham Lincoln y dispone del conocimiento necesario para ello. Si decide emplear una bsqueda DFS en sentido inverso (es decir, analizando padres, abuelos, bisabuelos etc. con la esperanza de encontrar a Lincoln) una bsqueda DFS podra retrotraerse hasta la prehistoria an en el caso altamente improbable de que s fuera descendiente directo. Dicho en otros trminos, si el algoritmo DFS se ejecuta indefinidamente, no es posible concluir ni a favor ni en contra de la premisa de partida. Por el contrario, el requerimiento en memoria es muy modesto. Como puede verse en la figura, DFS solo necesita almacenar un nico camino desde el nodo raz al nodo actual junto con todos los nodos sucesores generados por ese camino. As pues, el problema de DFS reside en el tiempo de cmputo pero no en la cantidad de memoria que necesita para su ejecucin. La figura 4 muestra una traza completa del algoritmo DFS para un problema de enrutamiento. Como puede apreciarse, la lista ABIERTOS coincide en todo momento con la frontera de la bsqueda. Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

119
CERRADOS {} {a} {b, a} {d, b, a} {e, d, b, a} {c ,e, d, b, a} {a} {b, c} {d, e, c} {e, c} {c} {}

a b d e c

N it. 0 1 2 3 4 5

ABIERTOS

Figura 4. Traza de una bsqueda primero en profundidad sobre el espacio de estados que aparece a la izquierda.

7.3. BSQUEDA PRIMERO EN ANCHURA


Aunque esta seccin est dedicada a una implementacin prctica de la bsqueda DFS, resulta interesante compararla con otra tcnica de bsqueda sistemtica desinformada denominada primeroenanchura. En este caso, la estrategia de seleccin de nodos consiste en elegir aquel candidato en ABIERTOS que se encuentre a menor profundidad. Intuitivamente, el rbol se genera horizontalmente o en anchura lo que da el nombre a esta tcnica. La implementacin primeroenanchura (BFS, del ingls BreadthFirstSearch) es esencialmente idntica a la bsqueda DFS solo que, en este caso, los nodos que entran deben extraerse de la lista en primer lugar, es decir, la lista ABIERTOS es, en este caso, un cola FIFO. Si se modifica la lnea 5 del pseudocdigo DFS para que los sucesores de X se almacenen en ABIERTOS por la derecha entonces se transforma en una bsqueda primeroenanchura. La figura 5 muestra la nueva traza para el mismo espacio de estados empleado en la figura 4.

a b d e c

N it. 0 1 2 3 4 5

ABIERTOS {a} {b, c} {c, d, e} {d, e} {e} {}

CERRADOS {} {a} {b, a} {c, b, a} {d, c, b, a} {e, d, c, b, a}

Figura 5. Traza de una bsqueda primero en anchura sobre el espacio de estados que aparece a la izquierda. Los nodos recin generados se incorporan a ABIERTOS por la derecha. Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

120

Al recorrer un espacio de estados primeroenprofundidad, la frontera de la bsqueda pasa a ser el conjunto de nodos en un mismo nivel del rbol y sus posibles sucesores (a diferencia de la bsqueda DFS que almacenada solamente el camino desde la raz). Este hecho hace que la memoria requerida por BFS sea exponencialmente mayor que la bsqueda DFS. Un ejemplo: el nmero de nodos hoja de un rbol uniforme con factor de ramificacin 5 y profundidad 6 es 56=15625, el orden de magnitud de los nodos que el procedimiento BFS debera mantener en memoria. Un algoritmo DFS equivalente necesitara aproximadamente 5x6=30, es decir 5 nodos por cada nivel. Sin embargo BFS, a diferencia de DFS, es completo. Esto quiere decir que garantiza encontrar una solucin al problema si sta existe. La demostracin es trivial. Si existe una solucin al problema, sta debe encontrarse en una profundidad finita del rbol d. Como BFS expande primero en anchura, completar el nivel d del rbol antes de pasar a niveles superiores, con lo que encontrar la solucin en un tiempo finito. Como se explic en la seccin 7.2, el algoritmo primero en profundidad puede perderse en una rama de profundidad infinita y nunca llegar a encontrar la solucin (en una profundidad d pero en un camino distinto). En teora de computacin es muy frecuente la dicotoma espaciotiempo. Un procedimiento que consume mucha memoria es, en la mayora de los casos, ms eficiente que un procedimiento equivalente que consume menos. La dicotoma es perfectamente aplicable a las tcnicas BFS y DFS.

7.4. METODOLOGA GENERAL DE RESOLUCIN DE UN PROBLEMA DE


BSQUEDA MEDIANTE COMPUTACIN
Antes de abordar el problema de la implementacin es importante destacar que existen un conjunto de consideraciones previas y tareas a realizar para la implementacin de un procedimiento eficiente que resuelva un problema de bsqueda genrico. Dicho problema se presupone bien formado. Entre las tareas a realizar destacan: Definicin del problema de una manera formal: Por ejemplo, para un problema de enrutamiento definir con precisin las reglas de movimiento entre ciudades, para el puzzle8 el movimiento de las piezas en horizontal y vertical etc. Anlisis: En esta fase se estudia minuciosamente el problema ya formalizado para determinar aquellas caractersticas que puedan tener influencia en las tcnicas de bsqueda que se van a emplear en su resolucin. Por ejemplo, si existe una explosin combinatoria en el nmero de estados, y no se tiene conocimiento especfico del dominio para guiar la bsqueda, entonces BFS no es recomendable por que consume excesiva memoria. Aislamiento y representacin adecuada del conocimiento necesario: Entre otras tareas, resulta extremadamente relevante para la eficiencia global del procedimiento de bsqueda una representacin adecuada de Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

121

la nocin de estado y de los operadores que permiten la transicin entre estados. En la figura 6 se muestran dos representaciones alternativas de los operadores de movimiento para el puzzle8. Se deja al criterio del lector el decidir cul de las dos sera ms adecuada con vistas a la implementacin de un algoritmo de bsqueda para ese problema. Eleccin adecuada de la mejor tcnica de bsqueda: A partir de la informacin adquirida en las etapas anteriores se decide cul de las numerosas tcnicas de bsqueda de propsito general es ms adecuada para el problema particular. Si la eleccin se restringe a una bsqueda DFS o BFS, un tamao de espacio de estados grande apunta hacia la tcnica DFS, a expensas de perder completitud en el caso peor. Para tamaos razonables de espacio de estados se aconseja la bsqueda primero en anchura que es completa.

1 8 6

2 3

7 4 5

1 8 6

2 3

7 4 5

Figura 6. Ejemplo de dos representaciones de los operadores de movimiento en el puzzle8: las piezas hacia el cuadro vaco (derecha) o el cuadro vaco hacia las piezas (izquierda).

7.5. IMPLEMENTACIN DE UNA BSQUEDA DFS MEDIANTE


RECURRENCIA
La recurrencia es una tcnica de programacin que consiste en especificar la ejecucin de un proceso mediante su propia definicin. Un algoritmo recursivo es, por tanto, aqul que plantea la solucin a un problema en trminos de una llamada a s mismo, lo que se conoce como llamada recurrente (o recursiva). Existen ejemplos de recurrencia en todas las reas de las ciencias. En matemticas una funcin recursiva f(x) es f ( x) = 3 f ( x 3) . En informtica el ejemplo tpico para ilustrar recurrencia es un algoritmo para computar el factorial de un nmero como el que se muestra a continuacin:
//Procedimiento Factorial(n) int factorial(int n) { if(n<2) return 1; return n*factorial(n-1); }

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

122

Como puede apreciarse en el ejemplo, lo que realmente est ocurriendo es que en cada nivel de recursin el problema se va descomponiendo en problemas iguales pero de menor tamao (el problema original tiene tamao n, tras la primera llamada recursiva pasa a ser de tamao n1 etc.). El problema ms pequeo es el factorial de 1 que se resuelve directamente en la segunda lnea de la funcin. Es importante destacar que la lnea de cdigo
if(n<2) return 1;

no solamente resuelve el factorial de 1 sino que permite salir de la recursin. Sin una sentencia de control de este tipo, el procedimiento quedara indefinidamente atrapado en un bucle infinito, debido a la circularidad inherente a la tcnica. Es fundamental garantizar que la condicin de salida (en este caso n<2) se cumple en un nivel de recurrencia finito. En el ejemplo y puesto que la llamada a Factorial se realiza con un valor una unidad menos que en el nivel anterior, resulta evidente que la condicin de salida se va a cumplir siempre en el nivel de recursin n2 y, por tanto, el procedimiento tiene que terminar. La sencillez del procedimiento Factorial permite analizar fcilmente el flujo de ejecucin. En cada nueva llamada a Factorial el flujo entra por la primera lnea de la funcin (justo despus de {) y puede salir debido a la instruccin
return 1;

o bien por la instruccin


return n*factorial(n-1);

En ambos casos, el flujo contina en la funcin del nivel de recurrencia anterior justo donde se realiz la llamada; es decir, se devuelve el resultado de la operacin y el flujo completa le ejecucin de la ltima lnea. Esto se puede ver con ms claridad aadiendo una variable intermedia al cdigo de la siguiente manera:
//Procedimiento Factorial(n) int factorial(int n) { int resultado; if(n<2) return 1; resultado = factorial(n-1); return n*resultado; }

Como resumen, al emplear recurrencia hay que tener en cuenta siempre que el flujo de ejecucin cumple con las especificaciones del problema, prestando especialmente atencin a la condicin de salida.

7.5.1 La pila de llamadas


Al ejecutar cualquier proceso, los sistemas operativos le asignan un espacio en memoria para cubrir sus necesidades, espacio que no puede ser utilizado por el resto de procesos en ejecucin. Este espacio reservado se conoce como rea de memoria del proceso.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

123

La pila de llamadas es una pila de datos LIFO en el rea de memoria de un proceso. La funcin principal es almacenar el punto donde devolver el control del flujo de ejecucin una vez terminada la funcin (o subrutina) activa en ese momento. De esta manera se pueden invocar funciones dentro de otras funciones sin perder el hilo de ejecucin. Cada nueva llamada introduce la direccin de retorno a la funcin invocante en la pila y empuja al resto de direcciones. Al terminar dicha funcin se lee la primera direccin de la pila como punto de retorno. Una de las ventajas adicionales de la pila de llamadas es que soporta recurrencia. Para la pila, el hecho de que una funcin A llame a una funcin B o se llame a s misma es irrelevante; basta almacenar en la pila la direccin de la instruccin siguiente a ejecutar una vez termine. En el ejemplo del Factorial, la direccin correspondera a la lnea
resultado = factorial(n-1);

Adicionalmente la pila de llamadas puede emplearse, entre otras cosas, para almacenar de forma eficiente las variables locales pertenecientes a la funcin activa. Estas variables pierden su valor una vez que termina la funcin. La pila puede realizar esta reserva de forma muy eficiente, reubicando el puntero de pila. Como desventaja, hay que decir que el rea de memoria reservada para la pila es bastante limitada. Cuando se sobrepasa aparece el tpico error en tiempo de ejecucin de desbordamiento de pila (o stack overflow), bien conocido por los programadores. Como ejemplo compile y ejecute este cdigo escrito en C++:
#include <iostream.h> #define MAX_SIZE 100 #define MAX_DEPTH 100 void ProcRecursivo(int k) { int vector[MAX_SIZE][MAX_SIZE]; if(k>=MAX_DEPTH) return; //Salida for(int i=0; i<MAX_SIZE; i++) for(int j=0; j<MAX_SIZE; j++) vector[i][j]=0; cout<<"Nivel: "<<k<<endl; ProcRecursivo(k+1); } void main() { cout<<"Comienzo de recursion"<<endl; try{ ProcRecursivo(0); } catch(...){ cout<<"Stack Overflow"<<endl; } cout<<"Fin de recursion"<<endl; }

Este cdigo dispone de una funcin ProcRecursivo que se llama de forma recursiva y que tiene como nica misin inicializar una matriz de enteros a cero en cada nivel de recurrencia. Obsrvese que, al estar cada matriz declarada localmente, el compilador, por defecto, reservar espacio en memoria en la pila de llamadas. Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

124

ProcRecursivo se llama a s mismo incrementando previamente en una unidad el valor que controla la salida de la recursin. Cuando la recurrencia alcanza el nivel MAX_DEPTH se produce la primera vuelta atrs. A partir de este momento se va liberando de forma secuencial la pila de llamadas hasta retornar al nivel de recurrencia 0 de partida. La funcin main llama a ProcRecursivo. Las instrucciones try y catch en C++ (y en muchos otros lenguajes de alto nivel) sirven para gestionar la aparicin de excepciones en tiempo de ejecucin. try encapsula entre llaves aquellas instrucciones susceptibles de producir algn tipo de excepcin y catch encapsula entre llaves las tareas a realizar si se producen (los manipuladores de las excepciones). Finalizado el bloque catch, el flujo de ejecucin contina con la siguiente instruccin despus del bloque. La sintaxis
catch(...)

indica que el bloque contiene los manipuladores para cualquier tipo de excepcin (incluyendo excepciones especficas de C). En el ejemplo, la excepcin slo puede producirse por desbordamiento de pila. En tal caso aparecera el mensaje Stack Overflow en pantalla para despus continuar con la ejecucin de la instruccin que muestra la cadena Fin de recursin en pantalla. La ejecucin del cdigo anterior para valores de MAX_DEPTH = 100 y MAX_SIZE=100 (Pentium D@3GHz, 1GB RAM sobre Windows XP) ya produce desbordamiento de pila utilizando el compilador Visual Studio 6.0. Por defecto el compilador otorga 1 MB de memoria a la pila de llamadas, y en este caso, la memoria 100 x100(matriz ) x100( nivel ) x 2(int) = 2 MB producindose el ocupada es desbordamiento. Si se modifican las opciones del compilador y se reserva 10MB de memoria virtual para la pila (opcin /stack:0x10000000) ya no se produce la excepcin. Es interesante comentar que el desbordamiento de pila tampoco se va a producir si se compila con la opcin de mxima velocidad (/O2) debido a que una de las optimizaciones que realiza el compilador es reservar memoria para la matriz bidimensional fuera de la pila. Con esta opcin de compilacin activada, el tamao de la matriz vector deja de constituir un problema para valores de MAX_SIZE muy superiores a 100.

7.5.2 Bsqueda DFS como recursin


El algoritmo para un procedimiento genrico de bsqueda primero en profundidad descrito en la seccin 7.2 estaba formulado de manera iterativa sobre una estructura de datos LIFO. En cada iteracin se va modificando la lista ABIERTOS hasta que, o bien se elige un estado OBJETIVO de dicha lista o bien la lista queda vaca, en cuyo caso el procedimiento termina sin encontrar una solucin. Para simplificar, se asume en este apartado que el grafo de bsqueda no tiene ciclos ni estados repetidos con lo que se puede prescindir de la estructura CERRADOS. Una manera alternativa de entender el procedimiento DFS es la de una recursin donde la tarea que se repite en diferentes niveles est formada por las subtareas siguientes:

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida Extraccin de un nodo de ABIERTOS (y eliminar) Generacin de sus sucesores Aadir dichos sucesores a la lista de ABIERTOS

125

A su vez, la salida de la recursin (vuelta atrs de la funcin activa en ese momento) se produce cuando: Se ha encontrado un estado OBJETIVO No se han encontrado sucesores para el nodo actual: lo que implica que es un nodo hoja y hay que volver atrs para continuar por otro camino

Si se estima que la dimensin del espacio de estados es relativamente pequea, se puede emplear la propia pila para almacenar los estados de ABIERTOS en el nivel del rbol que se corresponde con el nivel de recursin. Intuitivamente la lista ABIERTOS se divide por niveles y los nodos de cada nivel se declaran como variables locales en la pila de llamadas. El algoritmo recursivo DFS modificado para permitir que la pila de llamadas gestione la frontera de la bsqueda se muestra en la figura 7:
Procedimiento DFS_RECURSIVO (ACTUAL, OBJETIVO) Valor inicial: ACTUAL = Estado inicial 1. Si ACTUAL = OBJETIVO finalizar 2. Generar los hijos de ACTUAL y almacenar en L (variable local) 3. REPETIR hasta que L est vaco a. Seleccionar un nodo de L y llamarlo X b. DFS_RECURSIVO (X, OBJETIVO) c. Borrar X de L

Figura 7. Procedimiento recursivo primero en profundidad que permite emplear la pila de llamadas para almacenar la frontera de la bsqueda. Tomando como ejemplo el espacio de estados de la figura 4, la primera llamada a la funcin recursiva de bsqueda almacenara localmente los nodos {b,c}, hijos del nodo raz. Una nueva llamada pasando como parmetro el nodo b almacenara en la nueva lista local los nodos {d, e} descendientes directos el nodo actual. Al ser d un nodo hoja, la expansin de dicho nodo provoca que la funcin termine tras detectarse en la lnea 3 que no hay descendencia. Tras la vueltaatrs, la ejecucin continua por la lnea 3.c y se selecciona e el ltimo nodo abierto en este nivel de recurrencia (nivel 3 del rbol de bsqueda). Tras sucesivas vueltas atrs se expande el nodo c y finaliza la bsqueda. La ventaja del algoritmo de la figura 7 es que aprovecha la forma en que el Sistema Operativo gestiona la ejecucin de procesos en memoria para implementar la lista ABIERTOS en una bsqueda DFS. La frontera de la bsqueda se divide por niveles en el rbol y los nodos en cada nivel se almacenan por separado y de manera local a la correspondiente funcin. La pila de llamadas se encarga de borrar la estructura de Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

126

datos cuando se produce la vuelta atrs, una vez que se han analizado todos los nodos en el nivel de recurrencia actual. El procedimiento recursivo descrito para computar el factorial de un nmero puede verse tambin como una bsqueda en un grafo. Desde esta perspectiva, el espacio de estados tiene forma de rbol con una nica rama donde el estado inicial es el factorial del nmero buscado y el estado objetivo tiene el valor unidad (factorial de 1). El procedimiento recorre el rbol hacia delante hasta alcanzar dicho estado (nivel de recurrencia mxima). La solucin al problema se encuentra en el propio camino, y se genera durante las sucesivas vueltas atrs.

7.5.2.1.

Generacin de una clave

Como ejemplo sencillo de todo lo expuesto se propone como problema a resolver el encontrar la clave de un nmero de 6 dgitos entre 0 y 9 que controla el acceso a una cuenta de usuario en un servidor remoto. El procedimiento a realizar tiene que generar todas las combinaciones posibles de la clave (106) y bombardear al servidor. Se considera aqu solamente la rutina generadora de claves posibles. Este problema puede abordarse de forma trivial mediante un procedimiento iterativo empleando bucles anidados.; cada bucle genera un nmero de la clave y el bucle ms interior (en este caso el sexto) es el que genera la clave completa. La solucin en C sera la siguiente:
#include <iostream.h> #define TAM_NUMEROS #define TAM_CLAVE 10 6

void main() { int clave[TAM_CLAVE]; for(int i=0; i< TAM_NUMEROS; i++) for(int j=0; j< TAM_NUMEROS; j++) for(int k=0; k< TAM_NUMEROS; k++) for(int l=0; l< TAM_NUMEROS; l++) for(int m=0; m< TAM_NUMEROS; m++) for(int n=0; n< TAM_NUMEROS; n++){ //Generando clave clave[0]=i; clave[1]=j; clave[2]=k; clave[3]=l; clave[4]=m; clave[5]=n; cout<<i<<j<<k<<l<<m<<n<<endl; } }

Este problema puede tambin enfocarse como un problema de bsqueda y resolverse mediante la exploracin de un espacio de estados mediante la tcnica de primero en profundidad. Un posible cdigo para la implementacin recursiva que genera todas las posibles claves es:
#include <iostream.h> #define TAM_CLAVE #define TAM_NUMEROS 6 10

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


int clave[TAM_CLAVE]; void FuncRec(int depth) { if(depth == TAM_CLAVE){ //Salida de recursin for(int i=0; i<TAM_CLAVE; i++) cout<<clave[i]; cout<<endl; return; //Vuelta atrs } //Generacin de sucesores y llamada recursiva clave[depth]=-1; for(int j=0; j<TAM_NUMEROS; j++){ clave[depth]+=1; FuncRec(depth+1); } } void main() { FuncRec(0); }

127

Como en ejemplos anteriores existe una funcin (FuncRec) que de forma recursiva atraviesa el espacio de bsqueda de claves generando las 106 combinaciones. La configuracin del estado se almacena, en este ejemplo, en un vector global clave que es el que se va modificando en cada transicin. La verdadera clave es el valor de esta estructura de datos en un nodo hoja del rbol de bsqueda. La semntica detrs de cada nodo del rbol para una profundidad k es el conjunto de claves que tienen como valores en ndices 0, 1, 2,, k1 predeterminados por el camino desde el nodo raz hasta el nodo actual. El subgrafo que cuelga de dicho nodo conforma el espacio del resto de posibles claves con valores k , k + 1,L , tamao de clave 1 . Cuando la profundidad es exactamente 6 la construccin de la clave est completa. Entonces se presenta en pantalla y se produce la vuelta atrs:
if(depth == TAM_CLAVE) //Salida de recursin { for(int i=0; i<TAM_CLAVE; i++) cout<<clave[i]; cout<<endl; return; //Vuelta atrs }

La generacin de sucesores se lleva a cabo en las lneas de cdigo:


clave[depth]=-1; for(int j=0; j<TAM_NUMEROS; j++) clave[depth]+=1;

La primera instruccin inicializa la configuracin del estado en el nivel de profundidad siguiente (en el nivel de recursin k se generan los sucesores con valores de 0 a 9 en la posicin ksima de la clave). En este caso no se almacenan todos los sucesores localmente en cada nivel sino que segn se van generando se llama a la funcin de siguiente nivel. El cdigo completo de generacin y llamada es:

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


clave[depth]=-1; for(int j=0; j<TAM_NUMEROS; j++) { clave[depth]+=1; FuncRec(depth+1); }

128

El procedimiento es completo y para ello basta con analizar la forma del rbol de bsqueda. El rbol tiene 6 niveles de profundidad y en cada nivel, todos los nodos tienen exactamente 10 hijos (lo que se conoce como factor de ramificacin del rbol), por lo que en el ltimo nivel hay exactamente 106 nodos hoja que son el nmero de claves posibles a generar.

7.5.2.1.1 Comparativa entre ambos algoritmos


Desde la perspectiva de la complejidad computacional, el algoritmo iterativo es ms eficiente en tiempo ya que el algoritmo recursivo tiene que generar no solamente los nodos hoja sino el resto del rbol. El nmero de nodos totales N de un rbol uniforme con factor de ramificacin b y profundidad d es:
N = 1 + b + b 2 + b3 + L + b d 1 + b d

En el ejemplo b=10 y d = 6 con 106 hojas y 1+10+100+1000+10000+100000 =111.111 nodos adicionales hasta completar la totalidad del rbol (aproximadamente un 11%). Adems los compiladores modernos consiguen buenas optimizaciones de iteraciones pero no de recurrencias. En la parte positiva del cdigo recursivo cabe destacar: Es ms compacto: El nmero de sentencias que necesita es claramente ms corto y adems no depende del tamao de la clave. Lamentablemente no se puede decir lo mismo de la legibilidad. Es parametrizable completamente: El algoritmo iterativo permite definir un parmetro TAM_NUMEROS configurable pero no permite definir el parmetro TAM_CLAVE. Esto quiere decir que habr que aadir tantas sentencias for como nmeros tenga la clave, lo que no ocurre en la versin recursiva.

En cuanto a los requisitos en espacio, ambas implementaciones presentan un buen comportamiento. En el caso de la versin recursiva solamente se emplea la pila de llamadas para pasar el parmetro profundidad que es la nica informacin que se requiere para construir los nodos sucesores. Los diferentes estados se generan al vuelo actualizando una nica variable global clave. En este ejemplo la versin iterativa es ms intuitiva porque el problema de desciframiento de claves se presta a ello. Sin embargo, existen muchos otros problemas donde no es fcil, ni mucho menos intuitivo, implementar el control de las iteraciones para conseguir la solucin. Para estos problemas y debido al buen comportamiento de la bsqueda primero en profundidad en cuanto al consumo de memoria, el uso de recursin es preferible. Los algoritmos ms eficientes para muchos problemas NPDuros (como por ejemplo el problema del Mximo Clique) se implementan mediante esta tcnica.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

129

7.5.2.2.

Permutaciones

Un ejemplo ligeramente ms complicado es la generacin de permutaciones de N nmeros mediante una bsqueda recursiva primero en profundidad. En este caso el estado se almacena en un vector de N nmeros denominado permuta. En el nodo raz permuta tiene todos sus elementos a cero y contiene el valor de la permutacin en los nodos hoja. Como en el ejemplo anterior, los nodos intermedios del rbol sirven para ir rellenando la estructura de datos de forma adecuada. Para permutaciones de N nmeros, el rbol de bsqueda tiene profundidad N+1, donde el nivel 0 corresponde al nodo raz y el nivel N al de las N! hojas solucin. La funcin recursiva propuesta bien escribe el valor cero en el vector permuta o bien escribe el valor del nivel en el rbol del nodo actual. Un valor cero en permuta indica al generador de sucesores que esa posicin debe ser rellenada en niveles superiores y un valor distinto de cero determina el valor de la permutacin en esa posicin para cualquier nodo sucesor. De manera intuitiva, la funcin recursiva genera tantos sucesores como valores a cero (o huecos) tiene permuta en el momento de la invocacin. Inicialmente, permuta tiene todos los valores a cero con lo que tendr N sucesores en el nivel 1, lo que se corresponde con las diferentes posiciones del 1 en las N! permutaciones. En la llamada recursiva del nivel 2, permuta ya tiene puesto el 1 en alguna posicin con lo que el nmero de sucesores ser N1, las diferentes posiciones que puede ocupar el 2 en el conjunto de permutaciones posibles fijado ya el 1. La bsqueda contina expandiendo nodos hasta alcanzar las hojas en el nivel N, en cuyo caso permuta est completa (carece de huecos) y se produce la vuelta atrs. La figura 8 muestra el rbol de estados completo para el procedimiento propuesto con N = 3.
0
0 0 0

2 3

Figura 8. rbol de bsqueda para generar permutaciones de 3 nmeros mediante la tcnica de primero en profundidad. Como en el ejemplo anterior, ocurre que el nmero de estados generados es superior al nmero de permutaciones solucin. Para el rbol de la figura se puede demostrar que el nmero de nodos computados (llamadas a la funcin recursiva) ser ms del doble y menos del triple de las permutaciones posibles (por ejemplo, para N = 4 los nodos visitados son 65 y existen 24 permutaciones posibles). Una posible implementacin de la funcin recursiva que recorre el rbol de la figura 8 primero en profundidad es:

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


int nivel = -1; void FuncRec(int k) { nivel++; permuta[k] = nivel; if(nivel == N){ //Nodo hoja: Permutacin generada Mostrar(); } else{ for (int pos = 0; pos < N; pos++){ if (permuta[pos] == 0) FuncRec(pos); } } nivel--; permuta[k] = 0; }

130

La funcin emplea la variable global nivel para llevar la cuenta del nivel de profundidad del rbol generado y permuta para almacenar las permutaciones y guiar la bsqueda. La informacin pasada en cada llamada es la posicin en permuta donde se va a aadir el valor correspondiente al siguiente nivel de profundidad. Nada ms entra en la funcin se determina el primer estado sucesor:
nivel++; permuta[k] = nivel;

para despus comprobar si se est hoja, en cuyo caso se muestra la permutacin completa en pantalla:
if(nivel == N) Mostrar();

En caso de que el nodo actual no sea un nodo hoja se generan el resto de estados sucesores que, como se explic anteriormente, correspondern a valores nulos de permuta. Esta condicin se verifica justo antes de la expansin:
for (int pos = 0; pos < N; pos++) if (permuta[pos] == 0) FuncRec(pos);

Finalmente, tanto si es un nodo hoja como si no, se borra en el estado del nivel anterior la ltima modificacin de permuta para conseguir que el generador de sucesores en dicho nivel funcione correctamente. En el nivel 1 del rbol de bsqueda en la figura 8, esto equivale a borrar el 1 de permuta[0] justo antes de la vuelta atrs al nodo raz, para que el nuevo nodo sucesor sea en efecto permuta={0,1,0} y no permuta={1,1,0}. En este segundo caso, los sucesores que se generaran no seran correctos. El cdigo que realiza el borrado es:
nivel--; permuta[k] = 0;

Inicialmente nivel se inicializa a 1 para que la primera llamada a FuncRec corresponda con el nivel 0 que sirve como ndice de la primera modificacin de permuta. Permuta arranca con todo ceros. El cdigo completo que muestra todas las permutaciones de 4 nmeros en pantalla es el siguiente:
#include <iostream.h> #define N 4 int nivel=-1; int permuta[N];

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


void Mostrar(){ for (int i = 0; i < N; i++) cout<<permuta[i]; cout<<endl; } void FuncRec(int k){ nivel++; permuta[k] = nivel; if(nivel == N){ //Nodo hoja: Permutacin generada Mostrar(); } else{ for (int pos = 0; pos < N; pos++){ if (permuta[pos] == 0) FuncRec(pos); } } nivel--; permuta[k] = 0; } void main(){ for (int i = 0; i < N; i++) permuta[i] = 0; FuncRec(0); }

131

Cabe destacar que tanto en el generador de permutaciones como en el generador de claves no se ha seguido estrictamente en la implementacin el pseudocdigo descrito en la figura 7. En particular, no se ha empleado la pila de llamadas para almacenar toda la informacin de los estados frontera en cada nivel del rbol por dos razones: Era posible mantener una nica estructura de datos global y modificarla localmente para conseguir representar todos los estados del rbol de bsqueda y En ambos ejemplos se ha generado la informacin relativa a las transiciones de forma secuencial con las llamadas recursivas de forma que resultaba innecesario almacenar todos los estados nuevos de golpe.

En la prctica ambas condiciones no son demasiado frecuentes y es ms habitual encontrar implementaciones que siguen exactamente el pseudocdigo descrito en la figura 7, con la siguiente salvedad: si el tamao del espacio de estados es muy grande o si se busca mxima eficiencia, la reserva de espacio en memoria reservado para variables locales a la funcin recursiva resulta excesivamente lenta ya que se debe asignar y liberar en cada llamada. En estos casos, la solucin habitual pasa por reservar a priori el espacio en memoria para todos los estados del rbol (siempre que sea posible) antes de lanzar el procedimiento de bsqueda recursivo. En resumen, los identificadores que reservan espacio en memoria para la informacin de los estados del rbol deben ser globales a la funcin recursiva si se busca una mxima eficiencia.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

133

8.

EJECUCIN DISTRIBUIDA DE TAREAS

8.1. INTRODUCCIN
La problemtica de la ejecucin distribuida de tareas est hoy en da plenamente vigente despus del gran desarrollo que ha tenido Internet. En la prctica existen innumerables problemas computacionales donde se produce una fuerte explosin combinatoria que no son abordables adecuadamente por una nica unidad de proceso. Para estos casos, el rpido desarrollo de Internet est llevando, cada vez ms, al empleo de los tiempos muertos de la ingente capacidad de procesamiento conectada a la red para realizar, lo que podra denominarse, supercomputacin distribuida. Entre los numerosos ejemplos de este tipo de procesamiento cabe destacar el cmputo del genoma humano. El problema de la computacin distribuida o descentralizada est estrechamente ligado con el de la computacin paralela. En este caso, los avances tecnolgicos han permitido la aparicin de nuevos procesadores formados por mltiples ncleos (unidades de procesamiento) que ya se comercializan a gran escala. Por ejemplo, los procesadores Cell, desarrollados conjuntamente por Sony, IBM y Toshiba en el 2001, aceleran notablemente aplicaciones de procesado de vectores y multimedia. La videoconsola PlayStation3 de Sony fue su primera gran aplicacin. Otro ejemplo interesante es el gran avance que han tenido la arquitectura de las tarjetas grficas modernas, hasta el punto de que muchos clculos pueden llevarse a cabo ahora ms rpidamente por su unidad de procesamiento (conocida como GPU), en comparacin con las CPUs tradicionales. Tanto la computacin distribuida como la computacin en paralelo se basan en la descomposicin del procedimiento a realizar en subtareas, lo ms independientes posibles, de tal modo que la solucin final se pueda generar con cierta facilidad a partir Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

134

de las soluciones de cada una de las partes. Existen problemas fcilmente paralelizables (como por ejemplo la generacin de claves o el problema de las N Reinas) y otros mucho menos aptos para ello (e.g. muchos problemas no triviales de optimizacin o el algoritmo de bsqueda minimax). Las dificultades de trocear un problema en partes adecuadas para su computacin por separado se pueden clasificar en 3 grandes grupos. Estos son: Necesidad de comunicacin entre las unidades de procesamiento, con el consiguiente incremento en el tiempo de cmputo total. Particiones no independientes: En la mayora de problemas importantes es casi imposible un fraccionamiento en partes totalmente independientes. En el caso general, aparecen problemas de sincronizacin derivados de que unas unidades de procesamiento necesitan esperar la finalizacin de otras para continuar. Repeticin de tareas: En muchos casos no se puede evitar fraccionamientos con solapamiento. Esto hace que se pueda estar ejecutando a la vez la misma tarea en diferentes unidades de procesamiento.

En este captulo se muestra detalladamente un ejemplo de computacin distribuida para un problema clsico del mundo de los juegos: el problema de las N Reinas.

8.2. EL PROBLEMA DE LAS NREINAS

8.2.1 Historia
El problema de las 8Reinas consiste en colocar en un tablero de ajedrez de dimensiones 8x8, ocho reinas tal que ninguna se ataque entre s de acuerdo con las reglas del ajedrez. La generalizacin del problema a un tablero de dimensiones N x N se conoce como el problema de las NReinas. Este problema fue publicado por primera vez de forma annima en la revista alemana Schach en el ao 1848; posteriormente se le atribuy a un ajedrecista del momento, Max Bezzel, del que poco ms se conoce. Ya en aquel tiempo atrajo la atencin de la lite matemtica, entre los que se inclua el gran Carl Friedrich Gauss, que intent enumerar todas las distintas soluciones al problema. Gauss slo pudo encontrar 72 configuraciones distintas, lo que da una idea de la dificultad de este problema aparentemente sencillo. Solo unos aos ms tarde, en 1850, Nauck public las 92 soluciones del problema. En 1901, Netto por primera vez generaliz el problema a encontrar N reinas en un tablero N x N, aunque otras fuentes atribuyen al propio Nauck ese honor.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

135

8.2.2 Caractersticas
El problema de las NReinas es un problema terico que se enmarca dentro del rea de juegos. Ha sido un problema ampliamente estudiado desde la segunda mitad del siglo XIX y para el que se han descubierto algunas soluciones analticas cerradas; stas describen un procedimiento para obtener una o algunas pocas configuraciones objetivo para todo valor de N (N>3) (obviamente para N=1 la solucin existe y es trivial). Un ejemplo de solucin cerrada se enuncia a continuacin:
1. 2. en 3. 4. de Sea R la parte entera del resto de N/12 Sea L el conjunto de todos los nmeros pares de 2 (incluido) a N orden creciente. Si R es 3 o 9 coloque el 2 al final de la lista Aada a L (empezando por el final) el conjunto de nmeros impares 1 a N de acuerdo a las siguientes reglas: a. Si R es 8 intercambie parejas (por ejemplo 3,1,7,5,11,9,15,13...) b. Si R es 2 intercambie las posiciones del 1 y el 3 y coloque el 5 al final de L c. Si R es 3 9 coloque 1 y 3 al final de la lista manteniendo el orden 5. Coloque la primera reina en la casilla de la primera fila que indica el primer nmero de L; la segunda reina en la casilla de la segunda fila indicada por el segundo nmero de L y as sucesivamente.

Se anima a lector a emplear este procedimiento para encontrar una configuracin objetivo para valores de N bajos (por ejemplo N=10). Este y otros mtodos analticos permiten afirmar los dos siguientes postulados: 1. El problema tiene solucin para N =1 y para todo N mayor 3 2. Se sabe como construir al menos una solucin cuando sta existe Sin embargo, estos mtodos puramente analticos no son capaces de responder a ninguna pregunta acerca de la forma del espacio de estados del problema, ni tan siquiera proporcionar un conjunto de soluciones representativo de cada instancia. En el campo de la Inteligencia Artificial, el problema de las Nreinas se emplea como demostrador de prcticamente todas las tcnicas de bsqueda heurstica conocidas, dada la sencillez del enunciado y la tremenda explosin combinatoria que genera. Empleando tcnicas de mejora iterativa basadas principalmente en minimizacin de conflictos, Sosic y Gu a principios de los aos 90 pudieron ubicar ms de 3.000.000 de reinas en un tablero vaco; esta lnea de investigacin contina abierta en la actualidad. stas y otras tcnicas de bsqueda local, sin embargo, no permiten encontrar todas las soluciones del problema. Este ltimo es el escenario ms difcil ya que la explosin combinatoria que se produce empleando una bsqueda exhaustiva desinformada (e.g. un procedimiento primero en profundidad) es 2 2 N N ! , lo que para valores de N mayores de 30 resulta muy difcilmente = 2 N N !( N N )! Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

136

computable en la prctica. Esta cifra puede mejorarse mucho teniendo en cuenta que slo puede colocarse una reina por fila y por columna. An con todo, la bsqueda de todas las posibles configuraciones necesita de heursticas para atravesar el desierto formado por el gigantesco espacio de bsqueda y encontrar los oasis de soluciones. Una caracterstica singular de las Nreinas es que, si bien la dimensin del espacio de estados es claramente exponencial en el nmero de reinas a colocar, el nmero de soluciones tambin crece exponencialmente con N (ver Tabla 1). Tabla 1. Nmero de soluciones distintas del problema de las NReinas para diferentes valores de N.
4 5 6 7 8 9 10 11 12 13 14 15 2 10 4 40 92 352 724 2.680 14.200 73.712 365.596 2.279.184

Esta distribucin no es homognea en el espacio de estados sino ms bien existen enormes zonas vacas salpicadas de grandes concentraciones de soluciones. Intuitivamente esto quiere decir que, a mayor N, no es en absoluto evidente que el problema sea exponencialmente ms difcil (es ms, todo apunta a que esta afirmacin es falsa). A principios de los 90, Kal encontr una heurstica que permita computar las primeras 100 soluciones para cualquier N entre 4 y 1000 (ambos inclusive) en un tiempo casi lineal en N, por lo que conjetur que la densidad del espacio de soluciones podra ser uniforme. Recientemente, en una investigacin llevada a cabo por los propios autores se ha encontrado una nueva heurstica que corrobora esa afirmacin y extiende el cmputo a valores de N hasta 5000. Cuando se aborda el problema de las NReinas desde la perspectiva de la completitud se emplean fundamentalmente dos enfoques distintos: Conocer el nmero exacto de soluciones que existen para cualquier valor de N: En este enfoque interesa slo el nmero exacto de soluciones y no necesariamente su enumeracin explcita ni, desde luego, su almacenamiento (lo que sera, por otro lado, imposible dada la explosin combinatoria del nmero de soluciones). Enumerar las primeras K soluciones para cualquier valor de N: En este caso se exige el cmputo explcito. El valor de K no suele ser muy grande (por ejemplo 100), pero el suficiente para que el procedimiento de resolucin no pueda emplear mtodos analticos cerrados.

Fuera del mbito de los juegos, es interesante mencionar la aplicacin del problema de las NReinas en el campo de la optimizacin, donde constituye un importante modelo terico en problemas de planificacin (scheduling) y de asignacin de tareas (task assignment problems).

8.2.3 Estructuras de datos


La implementacin tpica del problema divide el tablero por filas y codifica una solucin cualquiera como N nmeros entre 1 y N que representan las casillas ocupadas Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

137

en cada fila, para un total de N! posibles configuraciones (las permutaciones de los N nmeros (ver figura 2)).

F4 F3 F2 F1 1 2 3 4

Figura 2. Solucin del problema de las 4Reinas. Dicha solucin puede codificarse como la cuadrupla {2, 4, 1, 3} que corresponde a la columna de la casilla ocupada en cada fila. En consecuencia, es suficiente un vector de N nmeros para codificar cualquier estado del espacio de soluciones y un procedimiento de bsqueda sistemtico (vlido) es cualquier algoritmo que genere permutaciones. Si se pretende abordar la generacin explcita de las primeras k soluciones, prcticamente la nica alternativa razonable es realizar una bsqueda primero en profundidad guiada por una heurstica adeudada. En este caso, la bsqueda no se debe desarrollar en un espacio de soluciones (como en el clculo de permutaciones) sino que cada estado del rbol se corresponde con una fila del tablero (o, alternativamente una columna), que se va rellenando hasta completar una solucin en los nodos situado a una profundidad N. De manera intuitiva en cada nivel del rbol se aade una reina al tablero hasta alcanzar una solucin. Si en un estado concreto no existen casillas libres en la fila o columna correspondiente se produce una vueltaatrs y la ltima reina colocada se elimina del tablero. Segn se expuso en la seccin 7.3, el control de la bsqueda slo requiere almacenar tanto el camino actual como todos los nodos sucesores directos de dicho camino. Si tomamos como factor de ramificacin medio del rbol (b) el valor de N/2, el espacio mximo requerido durante la bsqueda, teniendo en cuenta que la profundidad del rbol (d) no puede exceder de N, ser: Espacio mximo = N N N2 d = N = 2 2 2

lo que no supone mayor problema para los computadores actuales. Respecto a la generacin de los nodos sucesores a partir del padre, el mayor coste computacional reside en el clculo de las casillas atacadas tras colocar una nueva reina fruto de los rayos diagonales (las interacciones entre filas y columnas se pueden computar de manera sencilla actualizando una estructura de filas y columnas ocupadas). Para el cmputo eficiente de las casillas libres al vuelo es necesario aadir nuevas estructuras de datos como por ejemplo registros que llevan la cuenta del Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

138

nmero total de las casillas no atacadas en filas, columnas y diagonales etc. stas y otras estructuras bien elegidas permiten que la determinacin de las casillas libres se realice en tiempo constante pero requieren tiempo adicional para su cmputo. Es interesante mencionar que existen codificaciones ms o menos ingeniosas que asocian bits con caractersticas del dominio de manera que una operacin de enmascaramiento permite ejecutar varias operaciones en paralelo con significado en el problema. Estas estructuras pueden ser auxiliares (como por ejemplo emplear un vector de bits por cada diagonal del tablero, donde cada bit representa una casilla) o pueden estar en el corazn mismo del control de la bsqueda. El algoritmo elegido para implementar las NReinas que se describe en esta seccin emplea este tipo de codificacin.

8.3. IMPLEMENTACIN CENTRALIZADA


Se presenta en esta seccin un procedimiento que permite obtener, al menos en teora, todas las soluciones distintas del problema de las NReinas para valores de N hasta 32 (en la prctica esto no va a ser posible debido al crecimiento fuertemente exponencial de las soluciones con N segn muestra la tabla 1). El algoritmo genera explcitamente todas las posibles soluciones y lleva la cuenta del total. La restriccin en el valor de N se debe a que, para la codificacin de las casillas libres en una fila se emplea un nico entero de 32 bits, un bit por cada casilla de la fila. El control de la bsqueda se realiza mediante la tcnica primero en profundidad implementada de forma recursiva (ver seccin 7.5). Ms concretamente, el tablero se rellena por filas y la colocacin de una nueva reina en una fila provoca un cambio de estado; se puede decir, por tanto, que la bsqueda se realiza en un espacio de filas donde cada estadofila queda determinado por el nmero de casillas libres que dispone (aqullas casillas no atacadas por reinas ya presentes en el tablero). Los estadosfila sucesores se generan emplazando una nueva reina en cualquiera de las casillas libres del estadofila actual, con la particularidad que, debido a las estructuras de datos empleadas, las filas siempre se completan en direccin descendente empezando por la parte superior del tablero. Para aclarar estos conceptos, la figura 3 muestra una posible traza del rbol de bsqueda para el problema de las 4Reinas. Todos los nodos en un mismo nivel del rbol se corresponden con la misma fila del tablero, pero con diferentes distribuciones de casillas libres; el nodo raz del rbol, por tanto, corresponde al estadofila extremo superior del tablero que inicialmente est vaco. Los nodos hoja del rbol estn marcados con una cruz, con la excepcin del nodo hoja solucin que se encuentra en el 4 nivel. Los nodos hoja en niveles del rbol inferiores a N capturan el hecho de que una fila no tiene casillas libres, lo que supone un error en la ubicacin de una reina en niveles superiores y provoca una vuelta atrs. Se observa como la realizacin de la bsqueda en un espacio de filas en lugar de un espacio de soluciones permite podar la bsqueda reduciendo el tamao de rbol generado.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

139

nSol++

Figura 3. Traza del rbol de bsqueda del problema de las 4reinas. El nodo raz representa la fila superior del tablero. Estados marcados con una cruz son nodos hoja no solucin. Al encontrara una configuracin solucin se incrementa el contador nSol y contina la bsqueda. En el ejemplo, la bsqueda yerra al comenzar colocando una reina en la esquina superior derecha del tablero. Tras producirse la ltima poda en el nivel 3 (para la configuracin de reinas en estados superiores del camino no existen casillas libres en la fila actual) se produce una vuelta atrs. Posteriormente, tras encontrar una configuracin solucin (estado marcado con el parmetro nsol) se incrementa en una unidad la cuenta de soluciones y la bsqueda contina hasta que no existen sucesores que explorar o bien, en el caso general, el contador llega a un valor K.

8.3.1 Descripcin
En la implementacin propuesta, el control de la bsqueda obedece ntegramente al pseudocdigo propuesto para bsquedas primero en profundidad en el captulo 7. Las reinas se colocan por filas; para cada nuevo estado alcanzado se realizan las siguientes tareas en orden: 1. Comprobacin si el nuevo estado es solucin: Para ello basta analizar si el nivel de profundidad del rbol es N. En este caso se suma uno al contador de soluciones y se realiza una vueltaatrs para continuar por un nuevo camino. 2. Seleccin de la siguiente fila no ocupada: Las filas se rellenan de arriba abajo, empezando por la fila superior y terminando por la base del

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

140

tablero. En cada nivel del rbol se coloca una reina en alguna de las casillas libres de la fila correspondiente. 3. Generacin de los nodos sucesores: Para ello se calcula el nuevo conjunto de casillas no atacadas (libres) para la fila del siguiente nivel. Este es el proceso ms costoso en tiempo de cualquier implementacin y las estructuras de datos se eligen para minimizar dicho cmputo. Si no existen casillas libres en la fila elegida (y la profundidad del rbol es menor que N) entonces es que se ha producido un error en la colocacin de alguna de las reinas anteriores. Se efecta entonces una vueltaatrs al nodo padre para retomar la bsqueda. 4. Seleccin de un nodo sucesor NS de entre los generados en el paso 3. 5. Convertir NS en el estado actual e ir al paso 1: Este paso se implementa como llamada recursiva a la propia funcin encargada del procedimiento de bsqueda.

8.3.2 Estructuras de datos


Para optimizar el cmputo de las casillas libres en cada fila se ha empleado una codificacin mediante vectores de bits. Este tipo de codificaciones se utilizan con mucha frecuencia para tratar de reducir el tiempo de cmputo aprovechando que los registros de la CPU pueden efectuar un nmero de operaciones de enmascaramiento de bits en paralelo equivalente al tamao de los registros de la ALU (tpicamente 32 o 64). Intuitivamente, si se consiguen asociar bits a unidades de informacin acerca del dominio, entonces una sola operacin de enmascaramiento entre dos registros permite realizar 32 o 64 operaciones con sentido, con la consiguiente ganancia en eficiencia. Las estructuras de datos empleadas son: El tablero: La informacin del tablero, en cada nodo, se reduce a una fila, y ms concretamente a las casillas libres (no atacadas) de la fila. Cada fila se codifica como un nmero de 32 bits donde cada casilla equivale a un bit. Una casilla libre (no atacada) se codifica con un bit a uno y cero en caso contrario. La posicin relativa de los bits indica la posicin de la casilla en la fila; el bit ms bajo representa la columna ms a la derecha del tablero, el segundo bit la columna inmediatamente a la izquierda y as sucesivamente, para un mximo de N bits por fila, el nmero de columnas del tablero. El inconveniente principal de esta codificacin es que slo es vlida para tableros de dimensin 32 x 32 como mximo. Los movimientos de la reina: Los movimientos de la reina en el ajedrez (todas las casillas en las 8 direcciones en el plano) se van a codificar como operaciones de desplazamiento y enmascaramiento de bits. La idea fundamental es que slo es necesario computar las casillas atacadas en la fila correspondiente al estado actual y no las del resto de filas todava sin rellenar. El procedimiento cmputo se reduce a Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

141

generar las casillas libres en una determinada fila a partir del conocimiento de casillas libres en la fila inmediatamente superior. Para ello se utilizan 3 enteros izq, abajo y dcha. Una explicacin ms en detalle se expone en la seccin siguiente. Nmero de fila del nodo actual: Coincide con el nivel de profundidad del nodo en el rbol de bsqueda empezando la cuenta por el borde superior del tablero (fila 0) y terminando en la base (fila N1). Se almacena en un entero en cada nivel y se gestiona a travs de la pila de llamadas. Estructuras auxiliares: La configuracin de inicial de las casillas libres en una fila se guarda en la variable TODOUNOS. Este valor es constante durante toda la bsqueda y se calcula una vez al inicio. Otras estructuras son: un entero nSOL que lleva la cuenta del nmero de soluciones encontradas hasta el momento y la constante N que indica la dimensin del tablero.

8.3.3 Control de la bsqueda


La funcin recursiva que controla la bsqueda se denomina FuncRec. Su definicin es la siguiente:
void FuncRec(int fila, int izq, int abajo, int dcha) { int estado, sucesor; if (fila == N) { nSOL++; } else { estado = TODOUNOS & ~(izq | abajo | dcha); while (estado) { sucesor = -estado & estado; estado ^= sucesor; FuncRec(fila+1, (izq | sucesor )<<1, abajo | sucesor, (dcha | sucesor)>>1); } } }

Segn lo ya expuesto, los nodos del rbol de bsqueda son filas sin completar y para cada nuevo estadofila hay que de actualizar el conjunto de casillas libres (no atacadas) en esa fila. Esta actualizacin se realiza a partir de la informacin que el nodo padre pasa a su sucesor, los parmetros fila, izq, abajo y dcha. Inicialmente se comprueba si el nuevo estadofila es un nodo hoja solucin; para ello basta con saber si se ha alcanzado la profundidad mxima del rbol N. En caso afirmativo se suma uno al contador de soluciones nSOL y se vuelve atrs en el rbol para continuar la bsqueda. Esta comprobacin se realiza en la instruccin
if (fila == N) nSOL++;

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

142

Si la fila actual no es la ltima, entonces lo primero es generar de forma explcita el estado a partir de la informacin recibida del nodo padre. Esto se efecta mediante operaciones de enmascaramiento de bits en la lnea de cdigo
estado = TODOUNOS & ~(izq | abajo | dcha)

Esta instruccin requiere una explicacin ms detallada. El operador & en C es el operador binario AND a nivel de bits. Su resultado es un bit a 1 si en esa posicin los bits de ambos operandos estn tambin a uno. En caso contrario el bit toma el valor cero. Un ejemplo:
c1 = 0x45 01000101 c2 = 0x71 01110001

c1 & c2 = 0x41 01000001

El operador ~ es el operador unario complemento a uno en C. Como resultado, el nmero sobre el que opera intercambia los bits a cero por los bits a uno. La combinacin de operadores c1 & ~ c2 es interpretada por el compilador como c1 &(~ c2 ). El resultado es la puesta a nivel bajo de los bits de c1 que estn en la posicin ocupada por los bits a 1 de c2. Esta combinacin de operadores se conoce comnmente como borrado de c1 ya que el segundo operando lleva la informacin de los bits a borrar en el primero. Un ejemplo:
c1 = 0x45 01000101 c2 = 0x71 01110001

c1 &~ c2 = 0x04 00000100

Volvamos ahora al cmputo de las casillas libres en la nueva fila. La instruccin que genera el nuevo estadofila lo hace borrando aquellas casillas libres (inicialmente todas lo son por lo que estado coincide con TODOUNOS), que ahora resultan atacadas por reinas ya emplazadas en el tablero. Esta informacin est contenida en los parmetros izq, abajo y dcha que, de manera intuitiva, se corresponden con las casillas atacadas por todas las reinas ya colocadas, segn las tres direcciones del plano correspondientes (inferior izquierda, abajo, inferior derecha). No es necesario analizar los ataques en las otras 5 direcciones del plano porque las reinas se van colocando por filas en orden descendente y, por tanto, cualquier casilla atacada en la fila actual solo se puede deber a reinas situadas en filas superiores. Los 3 parmetros con informacin de casillas atacadas son enteros de 32 bits. Un bit a uno en cualquiera de ellos representa una casilla atacada por reinas situadas en filas superiores en la direccin correspondiente. La figura 4 muestra un ejemplo del valor de estas estructuras de datos para el problema de las 4Reinas. Una reina acaba de emplazarse en la fila superior y la nueva llamada recursiva a FuncRec recibe como Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

143

parmetros izq = 01002, abajo = 00102 y dcha = 00012, que corresponden a las casillas atacadas en las tres direcciones. El nuevo estado, las casillas libres en la fila inmediatamente debajo, se construye borrando todos esos bits de TODOUNOS (todas las casillas libres), lo que da como resultado una nica casilla libre estado = 10002 marcada por el cuarto bit a uno (la casilla del extremo izquierdo de la fila).
izq = 01002

Fnueva

izq

abajo dcha

abajo = 00102 dcha = 00012 estadonuevo = 10002

Figura 4. Valor de los parmetros izq, abajo y dcha tras colocar una reina en la fila superior del tablero para el problema de las 4Reinas. El estado en la fila nueva viene determinado por la operacin estado = 11112 &~ (izq | abajo | dcha) = 10002. Computado el estado actual de la fila, los posibles sucesores se obtienen situando una nueva reina en cualquiera de los bits a 1 de la variable estado. Esto se realiza de forma iterativa en el bucle determinado por
while (estado) { sucesor = -estado & estado; estado ^= sucesor; FuncRec(fila+1, (izq | sucesor )<<1, abajo | sucesor, (dcha | sucesor)>>1); }

que nuevamente requiere cierta explicacin. La primera lnea de cdigo, nada ms entrar en el bucle, obtiene el primer bit a 1 de la palabra estado mediante una ingeniosa pero muy conocida operacin a nivel de bits, combinacin de operadores y &:
sucesor = -estado & estado;

La operacin resta vista como operador unario calcula el complemento a 2 del operando al que afecta. La secuencia de operaciones - y & sobre un mismo nmero borra todos los bits exceptuando el bit a uno ms bajo de dicho nmero. Por ejemplo, -11012 & 11012 devuelve 00012 mientras que -11002 & 11002 devuelve 01002. El lector puede fcilmente comprobar que esta propiedad se cumple para cualquier nmero. Por tanto, sucesor ser un nmero formado por un nico bit a 1, el bit ms bajo de estado. La siguiente lnea de cdigo dentro del bucle completa el control del mismo.
estado ^= sucesor;

El operador ^ en C es la mscara XOR bit a bit, operador binario tambin conocido por distinto ya que mantiene a 1 aquellos bits que son diferentes en los dos operandos y borra los que son iguales. En este caso, como el nico bit a uno de sucesor tiene que estar en estado, el resultado es el borrado de ese bit en Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

144

estado. Intuitivamente, en cada iteracin se elige el bit a uno ms bajo de estado y despus se borra, lo que implica que las reinas se colocan de derecha a izquierda en las casillas libres de cada fila. Cuando estado est vaco finaliza la ejecucin del bucle. Es interesante hacer notar que el mismo resultado se obtendra mediante el operador ya visto de borrado:
estado &= ~sucesor;

pero sera menos eficiente ya que se necesita una operacin ms de enmascaramiento. Finalmente, decidido una vez el sucesor, es necesario actualizar las estructuras de datos izq, abajo y dcha antes de proceder a una nueva llamada recursiva. En el cdigo esto se realiza en la propia instruccin de llamada a la funcin:
FuncRec(fila+1, (izq | sucesor )<<1, abajo | sucesor, (dcha | sucesor)>>1);

El primer parmetro de FuncRec, el nmero de la fila, siempre se incrementa en una unidad. Su valor inicial es 0, la fila superior del tablero. El segundo parmetro es la actualizacin de la estructura izq a partir de su valor actual. Este cmputo puede verse como un desplazamiento hacia la izquierda una unidad de un nmero que tiene por bits a uno todas las columnas donde se encuentran las reinas ya colocadas en el tablero (incluyendo la ltima, en la fila actual y posicin sucesor) segn se desprende de la figura 5. En C, el operador desplazamiento a izquierdas tiene como smbolo <<. La sintaxis es la misma que la del operador de flujo de salida pero, en este caso, el operando de la derecha es un entero que indica el nmero unidades de desplazamiento de los bits del operando de la izquierda en la direccin apuntada por el smbolo.
izqact = 000102 estadoact = 111002 sucesor = 001002

Factual Fnueva 1 1

izqnue = (izqact | sucesor) <<1 = 011002

Figura 5. Actualizacin de la estructura de datos izq. izqact es el valor en la fila actual (Factual) e izqnue el nuevo valor calculado a partir del anterior. estadoact contiene las casillas libres en la fila actual. De entre stas, se ha elegido colocar una nueva reina en la casilla central de Factual, lugar que ocupa en la figura, almacenndose su posicin en la variable sucesor. Ahora bien, para obtener el nuevo valor de izq no basta con desplazar el antiguo una posicin a la izquierda (equivalente a izqnue = izqact <<1) ya que esta operacin tiene en cuenta los ataques en esta diagonal de todas las reinas situadas en filas anteriores a Factual pero no incluye la ltima que se encuentra en sucesor. De ah que izqnue sea compute a partir de la unin entre sucesor e izqact. Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

145

Un razonamiento anlogo puede hacerse para computar las casillas atacadas en la diagonal descendente dcha solo que, en este caso, el desplazamiento de bits es hacia la derecha una posicin (operador >> de C). En este punto es interesante destacar que la operacin de enmascaramiento que genera el estado actual al entrar en FuncRec:
TODOUNOS & ~(izq | abajo | dcha)

lleva implcita tambin la gestin de bordes. Este problema es inherente a los juegos de tablero y debe tenerse muy en cuenta en la seleccin de las estructuras de datos para codificar el problema. Como ejemplo, considrese el problema de las 4 Reinas nuevamente. Si se coloca una reina en la esquina superior izquierda, la codificacin de izqnue para la segunda fila sera 100002, pero al estar situada la reina en el extremo, ese bit a 1 queda fuera del rango de columnas del tablero. TODOUNOS es, en este caso, 11112 y lleva implcita la informacin del tamao del tablero. La mscara & ~ , por tanto, acta slo sobre las 4 casillas posibles de la fila resolviendo el problema de rangos de forma muy eficiente y elegante. Por ltimo, los nuevos ataques en la direccin vertical, sentido descendente (variable abajo) coinciden con el valor anterior aadiendo sucesor. Esto es as ya que el ataque a lo largo de cualquier columna corresponde al mismo bit en cada fila. La figura 6 muestra todas las estructuras de datos relacionadas con el cambio de estado para el ejemplo de la figura 4.

Factual Fnueva
izq abajo dcha

estadoact = 00002 sucesor = 00102 izqn = sucesor << 1 = 01002 dchan = sucesor >> 1= 00012 abajon = sucesor | 1 = 00102

estadonuevo = 11112 &~ (00102| 00012 | 00102 ) = 10002

Figura 6. Ejemplo de cmputo de casillas libres. La reina en la figura est codificada en sucesor y provoca la transicin a la fila nueva. La fila actual es el borde superior del tablero (nodo raz del rbol) y los valores de izqact, dchaact y abajoact en ese nodo son 00002. Los valores de izqn, abajon y dchan en la figura representan las casillas atacadas en la nueva fila.

8.3.4 Algoritmo de bsqueda


Una vez explicada en detalle la funcin recursiva principal que dirige la bsqueda, el resto de cdigo no ofrece especial dificultad. El cdigo completo para el problema de las 8Reinas es:

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


#include <stdio.h> #define N 8 //Max 32

146

const int TODOUNOS =(1 << N) - 1; int nSOL; void FuncRec(int fila, int izq, int abajo, int dcha) { int estado, sucesor; if (fila == N) { nSOL ++; } else { estado = TODOUNOS & ~(izq | abajo | dcha); while (estado) { sucesor = -estado & estado; estado ^= sucesor; FuncRec(fila+1, (izq | sucesor )<<1, abajo | sucesor, (dcha | sucesor)>>1); } } } int main(void) { nSOL = 0; FuncRec(0, 0, 0, 0); printf("N=%d -> %d\n", N, nSOL); return 0; }

El cmputo de soluciones lo lleva la variable nSol y el valor inicial de las filas TODOUNOS, ambas definidas como globales. Es interesante destacar las operaciones de bits que sirven para inicializar TODOUNOS:
const int TODOUNOS =(1 << N) - 1;

Primeramente se desplaza la constante 1 (que hay que visualizar como un nmero de 32 bits con el bit ms bajo a uno) N posiciones a la izquierda, con lo que se sita en la posicin N+1. Debido al acarreo, la operacin resta de una unidad convierte a unos todos los ceros a la derecha del uno desplazado. El uno en la posicin N+1 acta como barrera y evita, al ponerse a nivel bajo, la propagacin indebida del bit de acarreo ms all de su posicin. La llamada inicial a la funcin de bsqueda se realiza con todos los parmetros a cero (izq, abajo y dcha estn a nivel bajo al inicio). Con estos valores, el cmputo del estadofila en el nodo raz tiene tambin valor 0, o visto de otro modo, la primera reina puede emplazarse en cualquier casilla del borde superior del tablero vaco. Por ltimo, cabe destacar que la bsqueda que realiza este procedimiento es desinformada al no incorporar ninguna heurstica de decisin. Las reinas se emplazan en filas consecutivas en direccin descendente y se van colocando por columnas de derecha a izquierda (posiciones bajas a posiciones altas de bits a 1 en estado). Por Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

147

esta razn, y a pesar de que la codificacin admite tableros de dimensin hasta 32x32, valores de N superiores a 18 difcilmente pueden ser resueltos por un computador comercial con este algoritmo. A partir de N = 20 la tarea es prcticamente imposible.

8.4. IMPLEMENTACIN DISTRIBUIDA


El problema de las NReinas pertenece a la categora de problemas fcilmente paralelizables; basta considerar como particiones del espacio problema las casillas libres en cualquier fila. Resulta evidente que cada subproblema resultante de ubicar una reina en una casilla libre del estadofila actual es totalmente independiente del subproblema derivado de seleccionar otra casilla libre en la misma fila. En consecuencia, cada subrbol puede ser resuelto en paralelo sin necesidad de sincronizacin y con la seguridad de que no se estn repitiendo configuraciones solucin, un escenario idlico en el marco del cmputo paralelo. Se muestra en esta seccin una implementacin de esta paralelizacin tipo del problema de las NReinas, en el marco de un sistema distribuido. La implementacin se ha desarrollado para la plataforma Win32 y se emplean Sockets para establecer las comunicaciones entre los ordenadores remotos. El objetivo de este ejemplo, sin embargo, no es mostrar el empleo de Sockets en esta plataforma, sino el de presentar el potencial que tienen los sistemas distribuidos para resolver tareas en paralelo de forma ms eficiente que un sistema centralizado, al ser capaces de aprovechar el trabajo de mltiples unidades de proceso conectadas en red. Por este motivo, se asumir que existe una clase de tipo wrapper que encapsula los servicios del recurso Socket y que se encuentra a disposicin del programador mediante el mecanismo de herencia. En este sentido, la mayor parte de las explicaciones que aparecen en esta seccin pueden considerarse multiplataforma.

8.4.1 Arquitectura clienteservidor


La arquitectura distribuida elegida tiene a un cliente que centraliza la distribucin de la carga sobre un conjunto de servidores. El cliente se encarga de subdividir el problema en partes que sern resueltas por los diferentes servidores en la red; stos ltimos son los que ejecutan el algoritmo de bsqueda y devuelven como resultado al cliente el nmero de soluciones encontradas de cada problema parcial. El cliente, por su parte, tras finalizar el reparto de la carga, enva una peticin de resultado a los diferentes servidores cada segundo. Cuando todas las soluciones parciales han sido recibidas, muestra la suma total por pantalla. Con objeto de simplificar el ejemplo, la particin del espacio se ha realizado asignando en la primera fila (correspondiendo al borde superior del tablero) una casilla libre a cada servidor; ste resuelve el subproblema resultante tras la ubicacin de dicha reina en el tablero. En consecuencia, habr un mximo de N subproblemas a resolver y podrn existir un mximo de N servidores trabajando en paralelo. La figura 7 muestra la arquitectura descrita.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


Servidores Servidores

148

+ + + Cliente Cliente +

Figura 7. Arquitectura clienteservidor para el problema de las 4Reinas. El cliente divide el problema en 4 partes y recibe las soluciones de cada parte para generar el total.

8.4.2 Protocolo de comunicacin


La informacin que tiene que circular entre cliente y servidor es bastante escasa. En la etapa de reparto de carga el cliente solo tiene que enviar dos nmeros enteros: el tamao del problema (parmetro N) y la posicin de la reina en la primera fila (que determina la subtarea a resolver). Este parmetro se mide desde el borde derecho del tablero; para un tablero de lado N la esquina superior derecha tiene valor 0 y la esquina superior izquierda valor N1. El protocolo de este envo es una cadena de caracteres que tiene la forma siguiente:
CABECERA:Nqueens DATOS:<Tamao del tablero> <Casilla de la primera reina>

Este mensaje tiene acuse de recibo mediante la cadena OK por parte de cada servidor para indicar que se ha recibido satisfactoriamente. Una vez realizado el envo anterior, el cliente central lanza, cada segundo, una peticin de resultado a cada servidor y recibe de ellos un entero solucin si han terminado su parte. El mensaje de peticin de resultado es la cadena de caracteres Resultado. Cada servidor devuelve entonces la solucin obtenida o 1 si no ha terminado an. Cuando todos los mensajes de peticin han sido contestados satisfactoriamente, el cliente presenta la suma de los resultados en pantalla.

8.4.3 Implementacin del cliente


En esta seccin se describe en detalle todo lo relativo al funcionamiento de la parte del cliente. Como se indic la comienzo de esta seccin, se van a omitir la mayora de detalles acerca de los servicios de Win32 para Sockets. A todos los efectos, estos servicios se van a considerar transparentes para el programador y heredados de una clase Socket a su disposicin. En cambio, s se describir en detalle la manera de hacer uso de esta clase mediante el mecanismo de herencia.

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

149

8.4.3.1.

Comunicacin con el servidor

Las comunicaciones entre cliente y servidor se realizan a travs de Sockets. A efectos del ejemplo, bastar saber que existe una clase Socket que encapsula la recepcin y envo de mensajes y que tiene (entre otras) dos funciones miembro pblicas:
class Socket { public: int SendMsg(const char* cad, int length); int ReceiveMsg(char* cad, int* size, int timeout = 200); // }

que se encargan de las comunicaciones. La funcin SendMsg permite enviar una cadena de tamao length (medido en bytes) mientras que ReceiveMsg recibe una cadena de tamao mximo size. Ambos servicios devuelven un 0 si la comunicacin se ha efectuado con xito. Para el ejemplo se ha creado una clase cliente MyLiveClient que hereda estos servicios de comunicaciones mediante derivacin pblica de la clase Socket. Su fichero de cabecera es:
class MyLiveClient : public Socket { public: int RecibirResultado(); int EnviarNReinas(int size, int posq); MyLiveClient() {}; ~MyLiveClient(); };

Las dos funciones importantes de la clase son EnviarNReinas y RecibirResultado. La primera enva la informacin de la particin del problema (el tamao del tablero y la posicin de la reina en la fila superior) y la segunda realiza la peticin de resultado, ambas siguiendo el protocolo descrito en la seccin anterior. El cdigo de la funcin envo no requiere demasiado comentario:
int MyLiveClient::EnviarNReinas(int size, int posq) { char cad[100]; sprintf(cad,"Nqueens %d %d",size, posq); if(0!=SendMsg(cad,strlen(cad)+1)) return -1; int max_size=100; if(0!=ReceiveMsg(cad,&max_size)) return -1; cout<<cad<<endl; return 0; } //OK (-1 ERROR)

Se emplea el servicio SendMsg heredado de la clase Socket para realizar el envo de la tarea al servidor remoto y ReceiveMsg para gestionar un acuse de recibo que se muestra en pantalla. En ambos casos, el control de errores se gestiona a

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

150

travs del parmetro de retorno. La funcin strlen empleada dentro del segundo parmetro del servicio SendMsg devuelve el nmero de caracteres de la cadena argumento excluyendo el carcter nulo al final de la cadena. Este es el motivo por el que en la instruccin
if(0!=SendMsg(cad,strlen(cad)+1)) return -1;

hay que aadir una unidad al resultado de strlen. La implementacin de la funcin que pide y recibe el resultado es la siguiente:
int MyLiveClient::RecibirResultado() { // 0 OK, -1 ERROR char cad[100]; int max_size=10, res=-1; sprintf(cad,"%s","Resultado"); if(0!=SendMsg(cad,strlen(cad)+1)) return -1; if(0!=ReceiveMsg(cad,&max_size)) return -1; sscanf(cad,"%d",&res); if(res>=0){ cout<<"Recibido resultado correcto: "<<res<<endl; return res; } return -1; }

De nuevo el cdigo no requiere demasiada explicacin. Una vez enviada la peticin mediante el mensaje Resultado la instruccin
if(0!=ReceiveMsg(cad,&max_size)) return -1;

recibe en la cadena de caracteres cad la posible solucin numrica. Tras formatear la cadena como nmero (mediante el servicio sscanf), se comprueba que ste es mayor o igual que cero en cuyo caso se muestra un mensaje en pantalla y se devuelve su valor. En caso contrario la funcin devuelve 1 para indicar que la tarea no ha finalizado. Ntese que se acepta el valor cero como resultado porque pueden existir subproblemas sin ninguna configuracin solucin (e.g. N=4 con la primera reina situada en una de las esquinas).

8.4.3.2.

Hilo principal del cliente

El hilo principal del lado del cliente divide y enva cada subproblema a los servidores remotos. Para ello es necesario inicializar un recurso cliente por cada particin del problema y la comunicacin con cada servidor remoto se establece con un Socket distinto del lado del cliente. El hilo principal debe gestionar, por tanto, un vector de Sockets de tamao el nmero de particiones del problema. Para el ejemplo, se ha definido un parmetro global NUM_PARTES que, en tiempo de compilacin, proporciona las particiones deseadas. Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida El cdigo completo del hilo principal se detalla a continuacin.
#include <iostream.h> #include "MyLiveClient.h" #define NUM_PARTES 1 #define N 9 int main() { MyLiveClient client_array[NUM_PARTES]; //Arranque del vector de sockets for(int i=0; i<NUM_PARTES;i++) client_array[i].Init("127.0.0.1",12000+i); Sleep(1000); //Espera mientras arrancan los hilos de com.

151

//Enviar particiones for(i=0; i<NUM_PARTES; i++) client_array[i].EnviarNReinas(N ,i); //Recoger resultados cada segundo bool b_terminado; int sol[NUM_PARTES]; while(1){ Sleep(1000); //1 segundo por peticin b_terminado=true; for(int i=0; i<NUM_PARTES; i++){ if( (sol[i]=client_array[i].RecibirResultado())==-1) b_terminado=false; } if(b_terminado) break; } //Clculo de la solucin int total=0; for( i=0; i<NUM_PARTES; i++) total+=sol[i]; //Presentacin de la solucin cout<<"Numero de reinas: "<<total<<endl; //Cierre de sockets for( i=0; i<NUM_PARTES; i++) client_array[i].Close(); return 0; }

El vector de sockets est compuesto por objetos de la clase MyLiveClient que se crean e inicializan nada mas comenzar la ejecucin del hilo principal mediante la funcin heredada Init de la clase Socket. Las instrucciones de arranque son:
MyLiveClient client_array[NUM_PARTES]; for(int i=0; i<NUM_PARTES;i++) client_array[i].Init("127.0.0.1",12000+i);

La funcin Init requiere dos argumentos, la direccin IP del servidor remoto y el puerto. Como es lgico, ambos deben coincidir con el servicio de establecimiento de conexin en el servidor remoto. En el ejemplo, se emplea la direccin genrica IP

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

152

que existe en toda mquina para poder realizar pruebas en modo local y lo que cambia para cada Socket cliente es la configuracin del puerto (empezando por el 12.000). Posteriormente, el hilo principal del cliente realiza, cada segundo, la peticin del resultado a todos los servidores remotos mediante un bucle del que slo se sale si todos los servidores han finalizado. Esta funcionalidad se ha implementado de la manera ms sencilla posible y es manifiestamente mejorable (por ejemplo, no se distingue entre servidores que han finalizado el cmputo y los que no). El control de esta operacin se lleva a cabo mediante el flag b_terminado. Tras la recepcin de las soluciones parciales, el cliente calcula la suma total y muestra el resultado en pantalla. Finalmente, la funcin miembro Close es invocada para cada objeto MyLiveClient liberando el recurso Socket en memoria y cerrando su hilo de ejecucin. Esta funcin, al igual que Init, es heredada de la clase Socket mediante derivacin. La figura 8 muestra la traza de la sesin del cliente para el problema de las 8Reinas con la primera reina en la esquina derecha como nica particin (en este caso solo hay 4 soluciones). La comunicacin se establece localmente en el puerto 12000. La explicacin de la sesin es la siguiente: La lnea Connection indica que se ha establecido comunicacin con el servidor. Tras el envo de los datos correctos del problema, el servidor responde con un mensaje OK que se muestra en pantalla, de acuerdo con el protocolo implementado en la funcin miembro EnviarNReinas de MyLiveClient. Una llegada de una solucin mayor o igual que cero tras la peticin de resultado (mediante la funcin miembro RecibirResultado) muestra el mensaje Recibido resultado correcto: 4. El hilo principal sale entonces del bucle de peticiones. Se calcula la suma total y se muestra en pantalla (mensaje Nmero de reinas: 4). El cierre del socket cliente provoca una advertencia en pantalla de desconexin.

Figura 8. Traza de la sesin cliente para el problema de las 4Reinas con la primera reina en la esquina derecha como nica particin. Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

153

8.5. IMPLEMENTACIN DEL SERVIDOR


En el lado del servidor es donde se encuentra el procedimiento de bsqueda para el problema de las NReinas que llega desde el lado del cliente. El servidor empleado es sencillo y slo permite conexiones secuenciales de clientes; esto es, atiende a un cliente cada vez y al terminar queda a la espera de un nuevo cliente. En este caso slo est previsto un nico Socket cliente por sesin que pasa la informacin del problema y recoge el resultado. Esto slo es aceptable en el caso de disponer de todo el tiempo de procesamiento de los servidores remotos conectados, ya que la tarea pasada tiene una complejidad computacional elevada. En la prctica, sin embargo, raras veces se dar esta circunstancia y sera ms lgico una arquitectura que permitiera a los servidores un acceso simultneo a varios clientes.

8.5.1 Comunicacin con el cliente


De forma similar al caso del cliente, se dispone de la clase Socket que encapsula los servicios de comunicacin. Para la gestin del protocolo y el lanzamiento del algoritmo de bsqueda se ha desarrollado una clase MyLiveServer que hereda pblicamente de aqulla. Su fichero de cabecera (.h) es:
class MyLiveServer : public Socket { NQueen* m_pNQ; public: MyLiveServer(NQueen* pq); ~MyLiveClient(); virtual int OnMsg(char* cad,int length); };

La clase est lo ms desacoplada posible de la implementacin del procedimiento de bsqueda; la relacin se establece a travs del dato miembro privado m_pNQ que es un puntero a la clase NQueen que encapsula el algoritmo recursivo descrito con anterioridad. La direccin del objeto bsqueda se pasa en el momento de la llamada al constructor:
MyLiveServer(NQueen* pq);

Para la gestin del protocolo, MyLiveServer dispone de una funcin miembro OnMsg que es llamada cuando llega cualquier peticin del lado del cliente. Esta funcin est prevista en la arquitectura heredada y se sobreescribe aqu para implementar el protocolo. El calificativo virtual indica que se ha previsto polimorfismo para este servicio. La implementacin de OnMsg es la siguiente:
int MyLiveServer::OnMsg(char* cad,int length) { //LLamada a funcin heredada Socket::OnMsg() //Muestra el mensaje en pantalla cout<<"Ha llegado el siguiente mensaje: "<<cad<<endl; //Deserializacin int N, posq; char message[100]=""; char nombre[100];

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


sscanf(cad,"%s %d %d",nombre, &N, &posq); //Protocolo if(strcmp(nombre,"Nqueens")==0){ //Recepcin de tarea if((N<=0) || (N>=32) || (posq>N-1) || (posq<0) ){ sprintf(message,"%s","Error en Datos"); if( 0!=SendMsg(message,20) ) return -1; }else{ //OK m_pNQ->Set(N); m_pNQ->SetReinaPrimeraFila(posq); sprintf(message,"%s","OK"); if( 0!=SendMsg(message,20) ) return -1; } } else if(strcmp(nombre,"Resultado")==0){ //Envo de resultado sprintf(message,"%d",m_pNQ->GetCount()); if( 0!=SendMsg(message,10) ) return -1; } return 1; }

154

Al recibir un mensaje nuevo, la funcionalidad heredada llama al servicio OnMsg que extrae la informacin del mensaje prevista en el protocolo; se asigna a la variable local nombre la cabecera del mensaje, a la variable local N el tamao del tablero y a posq la posicin de la reina en la primera fila. Posteriormente se analiza la informacin recibida. Si la cabecera es Nqueens la peticin se reconoce como un envo
if(strcmp(nombre,"Nqueens")==0){}

mientras que si es una peticin de resultado se enva la informacin relativa a la solucin


if(strcmp(nombre,"Resultado")==0{}

En ambos casos se emplea la funcin strcmp que devuelve un cero si la cadena del argumento primero es exactamente igual que la del segundo. Detectada la peticin de ejecutar una tarea, se comprueban posibles errores en los parmetros y se actualizan los valores de la instancia de la clase NQueen que se encarga del procedimiento de bsqueda. Esta instancia se pas como puntero en el constructor del objeto MyLiveServer. La actualizacin de los datos se realiza en las instrucciones:
m_pNQ->Set(N); m_pNQ->SetReinaPrimeraFila(posq);

Caso de recibir una peticin de resultado, se llama a la funcin miembro GetCount() de la clase NQueen para obtener dicho valor y se enva como cadena al cliente:
sprintf(message,"%d",m_pNQ->GetCount()); if( 0!=SendMsg(message,10) ) return -1;

El significado de los parmetros de la funcin SendMsg es el mismo que en el caso del cliente, por lo que no se aade ningn comentario adicional. Por ltimo, destacar que si se detecta cualquier error en la transmisin de datos entre cliente y servidor, OnMsg retorna 1; si no de detecta ningn problema la funcin devuelve 1. Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

155

8.5.1.1.

Procedimiento de bsqueda

El algoritmo recursivo para las NReinas ya descrito tiene que modificarse ligeramente para recibir como parmetro la posicin de la reina en la primera fila. Para una gestin ordenada del procedimiento de bsqueda se ha definido la clase NQueen cuyo fichero de cabecera (.h) es el siguiente:
class NQueen { public: NQueen(); NQueen (int N); virtual ~NQueen(); void Reset(); void Set(int N); int SetReinaPrimeraFila(int posq); int GetSol(); int SolveQ(); private: void FuncRec(int fila, int izq, int abajo, int dcha); int m_TODOUNOS; int m_sol; int m_N; int m_posq; //0 a (N-1) };

Los datos miembro de la clase contienen la informacin inicial para el algoritmo tal y como se present en las secciones anteriores; m_N tiene el valor de N y m_TODOUNOS es un entero con los N primeros bits a 1 y el resto a cero. A stos se aade ahora m_posq que contiene la posicin de la reina en la primera fila, punto de partida de la bsqueda. La funcin miembro privada FuncRec lanza el procedimiento recursivo de bsqueda y es idntica a la ya descrita e el caso general. El interfaz de la clase consta de la funcin GetSol, que devuelve el valor solucin almacenado en m_posq, diversas funciones de inicializacin y el proceso que gestiona el inicio de la bsqueda SolveQ. El cdigo fuente de SolveQ es:
int { NQueen::SolveQ() int izq, dcha, abajo, pos; m_sol = 0; m_TODOUNOS = (1 << m_N) - 1; //Reina en la primera fila pos =(1<<m_posq); izq=pos <<1; dcha=pos >>1; abajo=pos; FuncRec(1, izq, abajo, dcha); return m_sol; }

Iniciados los parmetros m_sol y m_TODOUNOS, se procede de forma manual a ubicar la primera reina en la casilla m_posq del borde superior del tablero Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida

156

(fila 0). Para ello, basta con actualizar las estructuras de datos izq, dcha, y abajo que permiten computar el estadofila siguiente mediante las operaciones con bits ya explicadas anteriormente: pos=(1<<m_posq): Traduce la posicin relativa de la reina a la mscara con un nico bit a uno correspondiendo a esa posicin. La operacin de desplazamiento determine que m_posq tome valores entre 0 (pos = 0000..0012) y N1. izq=pos<<1: casilla de la segunda fila atacada por la reina situada en la casilla m_posq de la primera fila, considerando su movimiento en la direccin diagonal izquierda y sentido descendente. dcha=pos>>1: casilla de la segunda fila atacada por la reina situada en la casilla m_posq de la primera fila considerando su movimiento en la direccin diagonal derecha y sentido descendente. abajo=pos: casilla de la segunda fila atacada por la reina situada en la casilla m_posq de la primera fila considerando su movimiento en vertical y sentido descendente.

8.5.1.2.

Hilo principal del servidor

Una vez que se lanza el hilo de comunicaciones mediante la funcin heredada server.Init, el hilo principal del servidor entra en un bucle infinito y comprueba cada segundo si existe una bsqueda que completar. Para ello se ha elegido el valor del parmetro m_posq como elemento de comprobacin. Si recibe una tarea correcta, m_posq toma un valor mayor que cero. Si la tarea recibida no es correcta o ha terminado la bsqueda actual, m_posq toma el valor 1. La funcin main del servidor es la siguiente:
int main(int argc, char* argv[]) { NQueen queen; MyLiveServer server(&queen); server.Init("127.0.0.1",12000); while(1) { if(queen.GetPos()>=0){ //Comprueba si existe tarea queen.SolveQ(); cout<<"Solucion Encontrada: "<<queen.GetSol()<<endl; queen.SetReinaPrimeraFila(-1); //Fin de bsqueda } Sleep(1000); //Esperar 1 segundo } }

Los parmetros de la funcin server.Init() son la direccin IP y el puerto donde est escuchando el servidor. Los parmetros que figuran permiten realizar pruebas con la arquitectura clienteservidor en una sola mquina, para el cdigo del cliente descrito en la seccin anterior. El servicio Sleep (Win32) suspende la ejecucin del proceso que lo ejecuta durante el tiempo que figura como argumento (medido en milisegundos). Al terminar la bsqueda, la instruccin

Universidad Politcnica de Madrid UPM

RodrguezLosada & San Segundo, 2009. Programacin Avanzada, Concurrente y Distribuida


queen.SetReinaPrimeraFila(-1)

157

asigna el valor 1 al dato miembro m_posq. De esta manera se consigue que el hilo principal de ejecucin no entre en el bucle hasta que haya una nueva peticin del cliente ya que queen.GetPos() devuelve ahora como resultado 1. La figura 9 muestra la traza de la sesin del cliente para el problema de las 8Reinas con una reina situada en la esquina derecha del tablero como nica particin. La comunicacin se establece localmente en el puerto 12000. La explicacin de la sesin es la siguiente: Las dos primeras lneas de la sesin Comenzando Thread Server y Server: indican que se ha arrancado un Socket correctamente y que se encuentra a la espera de la llegada de un mensaje por parte del cliente. Esto se corresponde con la llamada a la funcin miembro heredada Init. La aparicin de ambos mensajes pertenece tambin a la funcionalidad heredada. Tras la llegada del mensaje con el problema a resolver, se llama a la funcin miembro OnMsg implementada en MyLiveServer. Esta funcin llama, a su vez, a la funcin OnMsg miembro de la jerarqua heredada (mensajes Client connected from: y Connection) y posteriormente muestra los datos recibidos en pantalla Al terminar el hilo principal el procedimiento de bsqueda recursivo, se muestra la solucin en pantalla
cout<<"Solucion Encontrada: 4"<<queen.GetSol()<<endl

Al llegar una peticin de resultado la funcin OnMsg muestra el mensaje en pantalla (Ha llegado el siguiente mensaje: Resultado). Al detectarse la desconexin del cliente lanza un mensaje de error y elimina el Socket de comunicacin abierto para l, quedando a la espera de la llegada de mensajes de nuevos clientes.

Figura 9. Traza de la sesin del servidor remoto correspondiente a la traza del lado del cliente mostrada en la Figura 8.

Universidad Politcnica de Madrid UPM

Vous aimerez peut-être aussi